loading...

.NET gRPC Server on Dapr runtime

thangchung profile image Thang Chung Updated on ・6 min read

Currently, there are a lot of people using gRPC/Protobuf protocol on the inter-communication inside the Kubernetes cluster because of small and reliability payload and format for the message transmit back and forth in the network (inside cluster). And it is actually faster than REST/JSON protocol when the payload is big and send many times because they use HTTP/2 by default in its server.

I also use gRPC/Protobuf in some of the projects and feel like a stable protocol with it by the time. In the fast, we use WCF/SOAP protocol and feel it really bloated and heavy. Now with gRPC/Protobuf, it brings not only a lot of benefits of simplicity of usage and implementation but also is really effective when running on production.

I move to Dapr in a few months, and I also really like the way Microsoft team approach for defining the model of development which helps a lot for developers to developing and deploy the cloud-native application.

But all examples for gRPC for .NET SDK in gRPC part is only the gRPC client to call to the gRPC server which is implemented in other languages such as Go, Nodejs... I know that we can implement the gRPC Server using C# language too. So that is a reason I write it out to help other people who want to use .NET to serve the gRPC protocol for other languages or can serve for .NET gRPC Client too.

Now is the time, I will walk through some of the important steps to make it work.
Go to the Dapr repository you can get the contract for the gRPC client and server. The .NET SDK for Dapr has already implemented the client proto. What we do here is implement the server proto to make it serve as the gRPC Server. The content of that file as below

syntax = "proto3";

package dapr;

import "google/protobuf/any.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/duration.proto";

option java_outer_classname = "DaprProtos";
option java_package = "io.dapr";

option csharp_namespace = "Dapr.Client.Autogen.Grpc";


// Dapr definitions
service Dapr {
  rpc PublishEvent(PublishEventEnvelope) returns (google.protobuf.Empty) {}
  rpc InvokeService(InvokeServiceEnvelope) returns (InvokeServiceResponseEnvelope) {}
  rpc InvokeBinding(InvokeBindingEnvelope) returns (google.protobuf.Empty) {}
  rpc GetState(GetStateEnvelope) returns (GetStateResponseEnvelope) {}
  rpc GetSecret(GetSecretEnvelope) returns (GetSecretResponseEnvelope) {}
  rpc SaveState(SaveStateEnvelope) returns (google.protobuf.Empty) {}
  rpc DeleteState(DeleteStateEnvelope) returns (google.protobuf.Empty) {}
}

message InvokeServiceResponseEnvelope {
  google.protobuf.Any data = 1;
  map<string,string> metadata = 2;
}

message DeleteStateEnvelope {
  string storeName = 1;
  string key = 2;
  string etag = 3;
  StateOptions options = 4;
}

message SaveStateEnvelope {
  string storeName = 1;
  repeated StateRequest requests = 2;
}

message GetStateEnvelope {
    string storeName = 1;
    string key = 2;
    string consistency = 3;
}

message GetStateResponseEnvelope {
  google.protobuf.Any data = 1;
  string etag = 2;
}

message GetSecretEnvelope {
  string storeName = 1;
  string key = 2;
  map<string,string> metadata = 3;
}

message GetSecretResponseEnvelope {
  map<string,string> data = 1;
}

message InvokeBindingEnvelope {
  string name = 1;
  google.protobuf.Any data = 2;
  map<string,string> metadata = 3;
}

message InvokeServiceEnvelope {
  string id = 1;
  string method = 2;
  google.protobuf.Any data = 3;
  map<string,string> metadata = 4;
}

message PublishEventEnvelope {
    string topic = 1;
    google.protobuf.Any data = 2;
}

message State {
  string key = 1;
  google.protobuf.Any value = 2;
  string etag = 3;
  map<string,string> metadata = 4;
  StateOptions options = 5;
}

message StateOptions {
  string concurrency = 1;
  string consistency = 2;
  RetryPolicy retryPolicy = 3;
}

message RetryPolicy {
  int32 threshold = 1;
  string pattern = 2;
  google.protobuf.Duration interval = 3;
}

message StateRequest {
  string key = 1;
  google.protobuf.Any value = 2;
  string etag = 3;
  map<string,string> metadata = 4;
  StateOptions options = 5;
}

In this article, we will implement the rpc InvokeService(InvokeServiceEnvelope) returns (InvokeServiceResponseEnvelope) {} method. Look at the request message as below

message InvokeServiceEnvelope {
  string id = 1;
  string method = 2;
  google.protobuf.Any data = 3;
  map<string,string> metadata = 4;
}

You can notice that there is a data with google.protobuf.Any type. So all the request will need to submit and fulfill the message transmit with google.protobuf.Any type. So we need to prepare the utility function to convert the DTO (data transfer object) to google.protobuf.Any type. The code as below

public static Any ConvertToAnyTypeAsync<T>(this T data, JsonSerializerOptions options = null)
{
    options ??= new JsonSerializerOptions
    {
        PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };

    var any = new Any();
    if (data == null)
        return any;

    var bytes = JsonSerializer.SerializeToUtf8Bytes(data, options);
    any.Value = ByteString.CopyFrom(bytes);

    return any;
}

And, we need to implement the utility function to convert back google.protobuf.Any type on the gRPC Server to a DTO too. The code as below

public static T ConvertFromAnyTypeAsync<T>(this Any any, JsonSerializerOptions options = null)
{
    options ??= new JsonSerializerOptions
    {
        PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };

    var utf8String = any.Value.ToStringUtf8();
    return JsonSerializer.Deserialize<T>(utf8String, options);
}

Now we need to refer to Grpc.Core-2.28.0-pre2, Grpc.AspNetCore-2.28.0-pre1, Google.Protobuf-3.11.4 and Grpc.Tools-2.28.0-pre2 nuget packages. Now we need to reference the Protobuf file in our project (.csproj) as below.

<ItemGroup>
    <Protobuf Include="..\..\_schema\proto\coolstore\dapr.proto" Link="Infrastructure\Protobuf\dapr.proto" AdditionalImportDirs="..\..\_schema\proto\" GrpcServices="Server" />
 </ItemGroup>

After doing that, we can implement the Dapr.DaprBase which is the server contract of Dapr gRPC part. The code as below

public class DaprService : CoolStoreDapr.Dapr.DaprBase
{
    private readonly IMediator _mediator;
    private readonly ILogger<DaprService> _logger;

    public DaprService(
        IMediator mediator,
        ILogger<DaprService> logger)
    {
        _mediator = mediator;
        _logger = logger;
    }

    public override async Task<CoolStoreDapr.InvokeServiceResponseEnvelope> InvokeService(
        CoolStoreDapr.InvokeServiceEnvelope request,
        ServerCallContext context)
    {
        _logger.LogInformation($"Id: {request.Id}, method: {request.Method}");

        var responseEnvelope = new CoolStoreDapr.InvokeServiceResponseEnvelope();

        switch (request.Method)
        {
            case "GetInventories":
            {
                var result = await _mediator.Send(new GetInventoriesQuery());
                _logger.LogInformation($"Got {result.ToList().Count} items.");
                var inventories = new List<InventoryDto>();
                inventories.AddRange(result);
                responseEnvelope.Data = inventories.ConvertToAnyTypeAsync();
                return responseEnvelope;
            }

            case "GetInventoriesByIds":
            {
                var queryRequest = request.Data.ConvertFromAnyTypeAsync<GetInventoriesByIdsQuery>();
                var result = await _mediator.Send(queryRequest);
                responseEnvelope.Data = result.ConvertToAnyTypeAsync();
                return responseEnvelope;
            }
        }

        return responseEnvelope;
    }
}

In the code above, we implement 2 functions: GetInventories and GetInventoriesByIds which are used with InvokeService method of gRPC Server. Now we need to register it with our .NET Host as below

public static IServiceCollection AddCustomGrpc(this IServiceCollection services,
            Action<IServiceCollection> doMoreActions = null)
{
    services.AddGrpc(options =>
    {
        options.Interceptors.Add<RequestLoggerInterceptor>();
        options.Interceptors.Add<ExceptionHandleInterceptor>();
        options.EnableDetailedErrors = true;
    });

    doMoreActions?.Invoke(services);

    return services;
}

and Program.cs file as following

internal class Program
{
    private static async Task Main(string[] args)
    {
        Activity.DefaultIdFormat = ActivityIdFormat.W3C;

        var (builder, configBuilder) = WebApplication.CreateBuilder(args)
            .AddCustomConfiguration();

        configBuilder.AddTyeBindingSecrets();

        var config = configBuilder.Build();

        Log.Logger = new LoggerConfiguration()
            .Enrich.FromLogContext()
            .WriteTo.Console()
            .CreateLogger();

        builder.Host
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.ConfigureKestrel(options =>
                {
                    options.ConfigureEndpointDefaults(o => o.Protocols = HttpProtocols.Http2);
                });
            })
            .UseSerilog();

        var connString = config.GetTyeSqlServerConnString("sqlserver", "inventorydb");

        builder.Services
            .AddLogging()
            .AddCustomMediatR(typeof(Program))
            .AddCustomValidators(typeof(Program).Assembly)
            .AddCustomDbContext<InventoryDbContext>(typeof(Program).Assembly, connString)
            .AddCustomGrpc();

        var app = builder.Build();

        app
            .UseRouting()
            .UseCloudEvents()
            .UseEndpoints(endpoints =>
            {
                endpoints.MapSubscribeHandler();
                endpoints.MapGrpcService<DaprService>();
            });

        await app.RunAsync();
    }
}

That is, and on the client, we can call to this gRPC server as below

public class InventoryGateway : IInventoryGateway
{
    private readonly IConfiguration _config;

    public InventoryGateway(IConfiguration config)
    {
        _config = config;
    }

    public async Task<IReadOnlyDictionary<Guid, InventoryDto>> GetInventoriesAsync(
        IReadOnlyCollection<Guid> invIds,
        CancellationToken cancellationToken)
    {
        var daprClient = _config.GetDaprClient("inventory-api");

        var request = new InventoriesByIdsDto();
        request.Ids.AddRange(invIds.Select(x => x.ToString()));

        var inventories = await daprClient.InvokeMethodAsync<InventoriesByIdsDto, List<InventoryDto>>(
            "inventory-api",
            "GetInventoriesByIds",
            request,
            null,
            cancellationToken);

        return inventories.ToDictionary(x => x.Id.ConvertTo<Guid>());
    }
}

In this case, we call to GetInventoriesByIds, we pass the list of inventory ids, the expect to get all of the inventory data out.

Alt Text

Let debugging it on gRPC Server - inventory service and gRPC Client - product catalog service. First of all at the terminal type

$ tye run --debug *

And set the break-points in 2 functions: GetInventoriesAsync - product catalog service and InvokeService - inventory service. Please see my previous article for how to make the debug works. Now run the application and submit the request to get a list of products with inventories associated with it. We have been jumped into inventory service

Alt Text

And after that, it should get the data out at product catalog service

Alt Text

The source code of this post:

That's it for today. Happy hacking!

Discussion

pic
Editor guide