BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Accessing .NET gRPC Endpoints from Anywhere via JSON Transcoding

Accessing .NET gRPC Endpoints from Anywhere via JSON Transcoding

Key Takeaways

  • JSON transcoding is a feature that has been added to gRPC in .NET 7. It allows gRPC endpoints to be accessible via a REST API, and it's much easier to set up than any alternative technology available at the time of writing, such as gRPC-Gateway and gRPC-Web.
  • Of course, there are some drawbacks to using JSON transcoding too. For example, it lowers the fidelity of the messaging contracts. 
  • With .NET 7, a developer simply has to include the reference libraries, add some options to our interface definitions, and apply a handful of lines of code to our request-processing middleware. 
  • Although JSON transcoding in .NET 7 is incredibly easy to apply, it has some significant limitations that developers must be aware of.
  • JSON transcoding requests are made over HTTP/1.1 rather than HTTP/2, which will inevitably lead to degraded performance compared to the standard gRPC requests; and JSON transcoding does not work with client streams. 
     

Introduction

gRPC is a communication protocol that enables applications to exchange direct messages in a very efficient manner. The efficiency is primarily achieved by two factors:

  • Protobuf, the serialization format that gRPC uses, is designed to ensure that each message occupies as little space as possible
  • gRPC is based on HTTP/2, so it uses its core features for improved performance, such as multiplexing

Unfortunately, not all client types can use the complete set of HTTP/2 features. This even includes commonly used client types, such as web browsers. Although most modern browsers support HTTP/2, they still don’t support specific HTTP/2 features that are needed for gRPC, such as multiplexing. Therefore not all clients can use the gRPC framework in its standard form. There are multiple ways to address this problem. But if our gRPC service implementation is written in .NET, the easiest solution by far is JSON transcoding.

But this is not the only benefit of using JSON transcoding. Even though gRPC has performance advantages over REST, which is one of the most popular communication mechanisms on the web, gRPC is harder to apply due to its strongly-typed schema and reliance on additional libraries both on the client and the server. And this is another problem that JSON transcoding solves.

JSON transcoding is a feature that has been added to gRPC in .NET 7. It allows gRPC endpoints to be accessible via a REST API. This makes the gRPC endpoints accessible from any HTTP client, including the browser. There is no need to duplicate gRPC endpoints as REST API endpoints. And it's much easier to set up than any alternative technology available at the time of writing, such as gRPC-Gateway and gRPC-Web.

gRPC-Gateway is similar to JSON transcoding in its philosophy and use cases. Both technologies turn gRPC endpoints into REST API endpoints. However, while JSON transcoding does it simply by adding a relatively lightweight library to the server application, gRPC-Gateway does it by running the requests via a reverse proxy. As well as being more difficult to set up than JSON transcoding, gRPC-Gateway is also less efficient, as there are some additional hops added to each request.

gRPC-Web applies a different philosophy; gRPC is implemented within the client, so the client makes standard gRPC calls to the server instead of REST API requests. But there are some major drawbacks to it too. First of all, a gRPC-Web client is significantly more challenging to configure due to its reliance on a strongly-typed message schema. It also requires the usage of external tools to generate suitable code stubs. Finally, just like gRPC-Gateway, it relies on a reverse proxy to translate the requests from HTTP/1.1 to HTTP/2 and vice versa, which reduces the performance.

With JSON transcoding, we don’t need to write separate REST API endpoints or perform some special setup on the client. All we need to do is add a handful of reference libraries, add some options to our interface definitions, and apply a handful of lines of code to our request-processing middleware. It’s really simple and this article will take you through the whole process.

Of course, there are some drawbacks to using JSON transcoding too. For example, it lowers the fidelity of the messaging contracts. Since it doesn’t enforce any schema on the client, it might not be suitable for certain use cases where a strongly-typed schema is preferred on the client, such as safety-critical applications. But due to its convenience of use, it would probably have more pros than cons compared to any other mechanism that enables gRPC on HTTP/1.1.

Now we will go through the process of using JSON transcoding in an ASP.NET Core app. We will use a gRPC service project that we will modify to enable JSON transcoding in it.

Prerequisites

Understanding ASP.NET Core fundamentals and a basic understanding of HTTP is absolutely essential to understand the information provided in this article. The article also assumes that the reader is already somewhat familiar with the gRPC implementation in .NET. If not, the official tutorial from Microsoft contains all the information you need.

If we want to replicate the setup from this article locally, we need to have .NET SDK 7 or newer installed on our development machine. This can be downloaded from its official page.

Finally, we need either an integrated development environment (IDE) or a code editor. Depending on your preferred operating system, the suitable options are Visual Studio, Visual Studio for Mac, Visual Studio Code, or JetBrains Rider, which can be downloaded from their respective websites.

Initial setup

We will start with an ASP.NET Core project based on the gRPC Service template. Our application represents a processor for a todo list. We can view the list, view each of its items, edit each item, add new items, and delete entries from the list. Our list is managed via the following interface:

namespace JsonTranscodingExample;

public interface ITodosRepository
{
    IEnumerable<(int id, string description)> GetTodos();
    string GetTodo(int id);
    void InsertTodo(string description);
    void UpdateTodo(int id, string description);
    void DeleteTodo(int id);
}

In this interface, we have a method for each action described above. The implementation of this interface would look as follows. We store todo items in a dictionary, where an integer is used as a unique key, similar to how an identity column works in a database.


internal class TodosRepository : ITodosRepository
{
    private readonly Dictionary<int, string> todos = 
        new Dictionary<int, string>();
    private int currentId = 1;

    public IEnumerable<(int id, string description)> GetTodos()
    {
        var results = new List<(int id, string description)>();

        foreach (var item in todos)
        {
            results.Add((item.Key, item.Value));
        }

        return results;
    }

    public string GetTodo(int id)
    {
        return todos[id];
    }

    public void InsertTodo(string description)
    {
        todos[currentId] = description;
        currentId++;
    }

    public void UpdateTodo(int id, string description)
    {
        todos[id] = description;
    }

    public void DeleteTodo(int id)
    {
        todos.Remove(id);
    }
}

We have a gRPC endpoint that allows us to access each of the methods outlined above. The Protobuf definition, which we have placed in a todo.proto file, looks as follows:

syntax = "proto3";

import "google/protobuf/empty.proto";

package todo;

service Todo {

  rpc GetAll (google.protobuf.Empty) returns (GetTodosReply);
  rpc Get (GetTodoRequest) returns (GetTodoReply);
  rpc Post (PostTodoRequest) returns (google.protobuf.Empty);
  rpc Put (PutTodoRequest) returns (google.protobuf.Empty);
  rpc Delete (DeleteTodoRequest) returns (google.protobuf.Empty);
}

message GetTodoRequest {
  int32 id = 1;
}

message GetTodosReply {
  repeated GetTodoReply todos = 1;
}

message GetTodoReply {
  int32 id = 1;
  string description = 2;
}

message PostTodoRequest {
  string description = 1;
}

message PutTodoRequest {
  int32 id = 1;
  string description = 2;
}

message DeleteTodoRequest {
  int32 id = 1;
}

This Protobuf definition is implemented by the TodoService class, which has the following content:

using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Todo;

namespace JsonTranscodingExample.Services;

public class TodoService : Todo.Todo.TodoBase
{
    private readonly ITodosRepository repository;

    public TodoService(ITodosRepository repository)
    {
        this.repository = repository;
    }

    public override Task<GetTodosReply> GetAll(
        Empty request,
        ServerCallContext context)
    {
        var result = new GetTodosReply();

        result.Todos.AddRange(repository.GetTodos()
            .Select(i => new GetTodoReply
            {
                Id = i.id,
                Description = i.description
            }));

        return Task.FromResult(result);
    }

    public override Task<GetTodoReply> Get(
        GetTodoRequest request,
        ServerCallContext context)
    {
        var todoDescription = repository.GetTodo(request.Id);

        return Task.FromResult(new GetTodoReply
        {
            Id = request.Id,
            Description = todoDescription
        });
    }

    public override Task<Empty> Post(
        PostTodoRequest request,
        ServerCallContext context)
    {
        repository.InsertTodo(request.Description);

        return Task.FromResult(new Empty());
    }

    public override Task<Empty> Put(
        PutTodoRequest request,
        ServerCallContext context)
    {
        repository.UpdateTodo(request.Id, request.Description);

        return Task.FromResult(new Empty());
    }

    public override Task<Empty> Delete(
        DeleteTodoRequest request,
        ServerCallContext context)
    {
        repository.DeleteTodo(request.Id);

        return Task.FromResult(new Empty());
    }
}

In this class, we are injecting the ITodosRepository interface. Then, each gRPC endpoint method calls an appropriate method on the interface and returns an appropriate response to the caller.

The content of our Program.cs file looks as follows. Here, we register all the appropriate gRPC dependencies, map the TodosRepository class as the implementation of the ITodosRepository interface for dependency injection, and register the gRPC endpoints of the TodoService class.

using JsonTranscodingExample;
using JsonTranscodingExample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();
builder.Services.AddSingleton<ITodosRepository, TodosRepository>();

var app = builder.Build();

app.MapGrpcService<TodoService>();

app.Run();

We can call the gRPC endpoints from any gRPC client that can fully function with HTTP/2. But what if we want to either call it from the browser or from a client that can only work with HTTP/1.1? This is where JSON transcoding comes into play. And we will add all the appropriate dependencies next.

Adding gRPC JSON transcoding dependencies

To enable gRPC JSON transcoding in our application, we will need to install the following NuGet packages:

Microsoft.AspNetCore.Grpc.JsonTranscoding
Microsoft.AspNetCore.Grpc.Swagger

The first NuGet package adds the core JSON transcoding functionality. The second package adds the ability to use Swagger with the REST API endpoints created from the gRPC endpoints, which automates the process of creating the API documentation.

Once we have installed these NuGet packages, we will the google folder inside the root folder of our project. Then we will create the api folder inside it. At the time of writing, we need to place some proto files into this folder structure. However, this will no longer be necessary in the future, as these files will be present in the framework itself.

First, we will need to create the http.proto file inside the newly created api folder. The content of the file can be copied from the following address.

However, the complete content with all comments removed can also be copied from here:

syntax = "proto3";

package google.api;

option cc_enable_arenas = true;
option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
option java_multiple_files = true;
option java_outer_classname = "HttpProto";
option java_package = "com.google.api";
option objc_class_prefix = "GAPI";

message Http {
  repeated HttpRule rules = 1;
  bool fully_decode_reserved_expansion = 2;
}

message HttpRule {
  string selector = 1;
  oneof pattern {
    string get = 2;
    string put = 3;
    string post = 4;
    string delete = 5;
    string patch = 6;
    CustomHttpPattern custom = 8;
  }
  string body = 7;
  string response_body = 12;
  repeated HttpRule additional_bindings = 11;
}

message CustomHttpPattern {
  string kind = 1;
  string path = 2;
}

Next, we will need to create the annotations.proto file inside the same folder. The content to populate it with can be copied from the following address

However, it is also available below with all the comments removed.

syntax = "proto3";

package google.api;

import "google/api/http.proto";
import "google/protobuf/descriptor.proto";

option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
option java_multiple_files = true;
option java_outer_classname = "AnnotationsProto";
option java_package = "com.google.api";
option objc_class_prefix = "GAPI";

extend google.protobuf.MethodOptions {
  HttpRule http = 72295728;

Once these two files have been created, we can open our original todo.proto file and add the following entry just above the package keyword:

import "google/api/annotations.proto";

Then, to add the JSON transcoding capabilities to all of our endpoints, we will need to modify each of the rpc definitions as follows:

rpc GetAll (google.protobuf.Empty) returns (GetTodosReply) {
  option (google.api.http) = {
    get: "/todos"
  };
}

rpc Get (GetTodoRequest) returns (GetTodoReply) {
  option (google.api.http) = {
    get: "/todos/{id}"
  };
}

rpc Post (PostTodoRequest) returns (google.protobuf.Empty) {
  option (google.api.http) = {
    post: "/todos/{description}"
  };
}

rpc Put (PutTodoRequest) returns (google.protobuf.Empty) {
  option (google.api.http) = {
    put: "/todos/{id}/{description}"
  };
}

rpc Delete (DeleteTodoRequest) returns (google.protobuf.Empty) {
  option (google.api.http) = {
    delete: "/todos/{id}"
  };
}

In each of these RPCs, we are adding an option that we export from the google.api.http package that is represented by the http.proto file we created earlier. Inside this option, we use an HTTP verb (get, post, delete, etc.) as the key. Then we have the URL path that will allow us to access the endpoint via a plain HTTP request. If we need to add a value representing a field in the request message of the gRPC endpoint, we surround the field name in curly brackets. For example, the following URL is accessible via a DELETE HTTP request.

delete: "/todos/{id}"

This path is what follows the base URL. For example, if the base URL of the application is https://localhost, then the full path that we should use to delete the item with the id of 1 is https://localhost/todos/1.  The {id} part of the path is a placeholder for the value of the id field from the request message type, which happens to be as follows:

message DeleteTodoRequest {
  int32 id = 1;
}

Next, we will need to add the appropriate configuration to the request processing middleware to enable JSON transcoding. To do so, we will first add the following statement at the beginning of the Program.cs file:

using Microsoft.OpenApi.Models;

Then we will locate the line that calls the AddGrpc method and add a call to the AddJsonTranscoding method, so it now looks as follows:

builder.Services.AddGrpc().AddJsonTranscoding();

Then, before we build the app variable from the builder variable, we add the following lines to import all Swagger dependencies:

builder.Services.AddGrpcSwagger();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1",
        new OpenApiInfo { Title = "TODO API", Version = "v1" });
});

Finally, we will add the following lines just after the app variable initialization to add Swagger endpoints to the middleware:

app.UseSwagger();
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});

That’s all we needed to do. JSON transcoding functionality is now fully enabled in our application. We can now start using it.

Calling gRPC endpoints via REST API

To test our application, we can launch it and send HTTP requests to any paths we mapped in the todo.proto file. There are multiple ways we can launch it. We can execute the dotnet run command inside the project folder. We can publish the application on a proper server. Or we can launch it from the IDE.

Once the application is launched, we can locate its base URL and send the HTTP requests to the mapped paths. If we are running our application in debug mode, the base URL can be found inside the launchSettings.json file in the Properties folder of the project. Then we can use any available tool to send requests to it, such as Postman, Fiddler, or curl. But the simplest way is to just launch its Swagger page in the browser, where each URL will be exposed in an intuitive user interface.

To launch the Swagger page, we can enter the base URL of the application followed by the /swagger path. We should be presented with a page that looks like the following:

All mapped API endpoints will be displayed on it. To send a request to any of them, we need to expand it and click the “Try it out” button. Then we can enter the parameters and click “Execute”. A correctly constructed HTTP request with an appropriate verb will then be made to the server. We will see the response displayed on the page in JSON format.

Key limitations of gRPC JSON transcoding

Although JSON transcoding is incredibly easy to apply, it has some significant limitations that developers must be aware of. The first one is the fact that the requests are made over HTTP/1.1 rather than HTTP/2. This will inevitably lead to degraded performance compared to the standard gRPC requests.

The second major limitation of JSON transcoding is that it doesn’t work with client streams. Therefore neither the client-streaming nor the bi-directional streaming calls would work with it. Server streams are supported, though. When used, the client will receive a collection of JSON objects as a response.

Both of these limitations dictate that JSON transcoding should only be used as a fallback when it’s impossible to work with standard gRPC. However, these limitations are equally applicable to both gRPC-Web and gRPC-Gateway. Therefore JSON transcoding is still a useful technology. And because it’s much easier to set up than any of the alternatives, it is probably the best choice when gRPC endpoints need to be reached from a client that can’t fully use HTTP/2.

About the Author

Rate this Article

Adoption
Style

BT