gRPC JSON Transcoding in .NET 7

2022, Dec 14

.NET 7 introduced gRPC JSON transcoding, used within ASP.NET Core, to allow HTTP/2 requests to be transcoded to JSON and transmitted via REST APIs on HTTP/1. This was introduced mainly because HTTP/2 is not fully supported in browsers, because JSON and REST APIs have an important role in modern apps, and because it would be too much for teams to build separate gRPC and REST Web APIs to cope with those browser limitations.

Overview

In previous posts we approached gRPC implementations using protobuf .proto files and protobuf-net, however, in .NET 7 the JSON Transcoding is implemented with the use of proto files.

Once implemented, JSON transcoding enables gRPC services to also become REST JSON APIs running on HTTP/1. Requests can then make use of:

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

The changes

The template ASP.NET Core gRPC Service in Visual Studio 2022 creates a gRPC boilerplate service that needs changing to work with JSON transcoding. To start with the NuGet package Microsoft.AspNetCore.Grpc.JsonTranscoding, which needs to be referenced, and then make sure it gets registered on startup code, by adding AddJsonTranscoding() as below:

using GrpcNet7JSONTranscoding.Services;

var builder = WebApplication.CreateBuilder(args);

// Additional configuration is required to successfully run gRPC on macOS.
// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682

// Add services to the container.
builder.Services.AddGrpc().AddJsonTranscoding();

var app = builder.Build();

// Configure the HTTP request pipeline.
app.MapGrpcService<GreeterService>();
app.MapGrpcService<UserService>();
app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client.");

app.Run();

Another change required is around the Protos folder, which needs to sit outside of the project, with the additional proto files:

Really weird I agree, a Google extension used with proto files. Make sure you have a folder structure like this:

--Proto
--|--google
--|--|--api
--|--|--|--annotations.proto
--|--|--|--http.proto
--|--address.proto
--|--user.proto
--|--greet.proto

Then reference the proto files on .csproj like this:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <Protobuf Include="..\Proto\greet.proto" GrpcServices="Server" Link="Protos\greet.proto" />
    <Protobuf Include="..\Proto\user.proto" GrpcServices="Server" Link="Protos\user.proto" />
    <Protobuf Include="..\Proto\address.proto" GrpcServices="None" Link="Protos\address.proto" />
    
    <PackageReference Include="Grpc.AspNetCore" Version="2.50.0" />
    <PackageReference Include="Microsoft.AspNetCore.Grpc.JsonTranscoding" Version="7.0.1" />
  </ItemGroup>

</Project>

In this example, we are going to make use of the proto file user.proto, used in previous posts. Here are some changes required to enable JSON transcoding. Let's compare the files:

File: user.proto (original)

syntax = "proto3";

option csharp_namespace = "GrpcServiceSample";

package user;

import "protos/address.proto";

service UserService {
    rpc GetUserDetails(UserRequest) returns (UserResponse);
}

message UserRequest {
    int32 user_id = 1;
}

message UserResponse {
    int32 user_id = 1;
    string first_name = 2;
    string last_name = 3;
    address.Address address = 4;
}

File: user.proto (changed)

syntax = "proto3";

import "google/api/annotations.proto";

option csharp_namespace = "GrpcServiceSample";

package user;

import "address.proto";

service UserService {
    rpc GetUserDetails(UserRequest) returns (UserResponse) {
      option (google.api.http) = {
       get: "/user/{user_id}"
      };
    }
}

message UserRequest {
    int32 user_id = 1;
}

message UserResponse {
    int32 user_id = 1;
    string first_name = 2;
    string last_name = 3;
    address.Address address = 4;
}

The proto file makes use of import "google/api/annotations.proto", which was added to the Proto folder, and within the RPC GetUserDetails the GET request is specified, accepting the URL parameter binding {user_id}.

Regarding the UserService, this is a dummy implemented inheriting from UserServiceBase:

using Grpc.Core;
using GrpcNet7JSONTranscoding.Protos;

namespace GrpcNet7JSONTranscoding.Services
{
    public class UserService : Protos.UserService.UserServiceBase
    {
        private readonly ILogger<UserService> _logger;
        public UserService(ILogger<UserService> logger)
        {
            _logger = logger;
        }

        public override Task<UserResponse> GetUserDetails(UserRequest request, ServerCallContext context)
        {
            var userResponse = new UserResponse
            {
                UserId = 1,
                FirstName = "Bill",
                LastName = "Lumbergh",
                Address = new Address()
                {
                    Number = "1A",
                    StreetName = "Initech Street",
                    Suburb = "Initech Suburb",
                    City = "Austin, Texas",
                    Country = "USA"
                }
            };

            return Task.FromResult(userResponse);
        }
    }
}

After that, the service can be launched and consumed by clients.

Running the service

Create a new Console Application, make sure to reference the gRPC NuGet packages and proto files, as available below:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Google.Api.CommonProtos" Version="2.7.0" />
    <PackageReference Include="Google.Protobuf" Version="3.21.11" />
    <PackageReference Include="Grpc.Net.Client" Version="2.50.0" />
    <PackageReference Include="Grpc.Tools" Version="2.51.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    
  </ItemGroup>

  <ItemGroup>
    <Protobuf Include="..\Proto\greet.proto" GrpcServices="Client" Link="Protos\greet.proto" />
    <Protobuf Include="..\Proto\user.proto" GrpcServices="Client" Link="Protos\user.proto" />
    <Protobuf Include="..\Proto\address.proto" GrpcServices="None" Link="Protos\address.proto" />
  </ItemGroup>

</Project>

Note: If you don't reference those NuGet packages, things don't work.

Then to request the server, this is the client implementation:

using Grpc.Net.Client;
using System.Text.Json;
using GrpcNet7JSONTranscoding.Protos;
using System.Net.Http;

var userId = 1;

// gRPC
var channel = GrpcChannel.ForAddress("https://localhost:7060");
var client = new UserService.UserServiceClient(channel);
var userDetails = await client.GetUserDetailsAsync(new UserRequest() { UserId = userId });
Console.WriteLine(JsonSerializer.Serialize(userDetails));

// REST API
var httpClient = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, $"https://localhost:7060/user/{userId}") { Version = new Version(2, 0) };
var response = await httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(content);

Console.WriteLine("Press any key to exit...");
Console.ReadKey();

It creates a request to the gRPC service and also to the REST API endpoint, one running on HTTP/2 and the other on HTTP/1. The result contains the same information, the first a Pascal-cased serialized object while the other a camel-cased JSON string:

results

Check this sample in the PlayGoKids repository.

For more information check the Microsoft Docs