In this post I will show how I created a gRPC based api using Asp.Net Core.

gRPC

gRPC is a remote procedure call based framework for creating highly optimized api endpoints with focus on low network latency. Unlike rest where the api is explicitly url based, gRPC implementations abstract away the details of remote communication. Instead of explicitly specifying a remote address to a remote server when invoking a remote function, the api is invoked much like local functions.

The local api acts like a facade on top of the underlying implementation, but how does this get wired together?

Luckily for us, we don’t have to hand code any of the remote protocol implementation. Instead we can generate most of the code based on a standard called protocol buffers. Protocol buffers are basically language agnostic schema definitions that can be used for code generation.

I have included my sample protocol buffer file below:

syntax = "proto3"; package Article; service ArticleService { rpc GetArticle (ArticleRequest) returns (ArticleReply) {} rpc SaveArticle (SaveArticleRequest) returns (SaveArticleReply) {} } message ArticleRequest { string id = 1; } message ArticleReply { string text = 1; string title = 2; string intro = 3; string key = 4; string url = 5; } message SaveArticleRequest { string text = 1; string intro = 2; string url = 3; string key = 4; string title = 5; } message SaveArticleReply { string Status = 1; }

As is often the case, my demos represent some feature I’m building for my blog.

In my schema definition I have included two service methods for saving and retrieving blog articles. This is the service I use when editing or creating new blog posts, like the one you are reading right now.

Each service method comes with a request/response object pair for defining the shape of the request and response payloads. The properties on the objects are typed with standard programming types, but what is the sequential number listed next to the properties? It turns out this number is just used for internal serialization, so we don’t really have to worry about it. Just make sure to avoid duplicates when specifying new properties.

Service

Now, whenever we build the project, the protocol definitions will be converted to actual code that can be used to create our service implementation.

I have included the implementations of my two service methods below:

public class ArticleService : Article.ArticleService.ArticleServiceBase { private MongoClient dbClient; private IMongoDatabase db; public ArticleService(IConfiguration configure) { this.dbClient = new MongoClient(configure.GetSection("MongoDb").Value); this.db = this.dbClient.GetDatabase("success"); } public override async Task GetArticle(ArticleRequest request, ServerCallContext context) { var a = await this.db.GetCollection ("articles").Find(article => article.friendlyUrl == request.Id).SingleOrDefaultAsync(); return new ArticleReply() { Title = a.title, Text = a.body, Intro = a.intro, Key = a.key, Url = a.friendlyUrl}; } public override async Task SaveArticle(SaveArticleRequest request, ServerCallContext context) { await this.db.GetCollection ("articles") .FindOneAndUpdateAsync(Builders .Filter.Where(a => a.friendlyUrl == request.Url), Builders .Update .Set(a => a.title, request.Title) .Set(a => a.body, request.Text) .Set(a => a.key, request.Key) .Set(a => a.intro, request.Intro) .Set(a => a.friendlyUrl, request.Url), new FindOneAndUpdateOptions () { IsUpsert = true } ); return new SaveArticleReply() { Status = "OK" }; } }

ArticleServiceBase is an example of generated code. The only custom code here is the MongoDB specific code for retrieving and saving articles.

Client

At this point we have a working service, but we need to wire up a client in order to make calls. There are many ways to do this, but in my example I have decided to wrap the client in a regular Asp.Net web api. I did this because I am integrating this with a Javascript based component, and I want to avoid exposing the gRPC bits to the web client.

I have included my web api methods below:

[HttpGet("{id}")] public async Task > Get(string id) { var channel = new Channel("localhost:50051", ChannelCredentials.Insecure); var client = new Article.ArticleService.ArticleServiceClient(channel); var reply = await client.GetArticleAsync(new ArticleRequest { Id = id }); await channel.ShutdownAsync(); return reply; } [HttpPost] public async Task > Post([FromBody] ArticleDto a) { var channel = new Channel("localhost:50051", ChannelCredentials.Insecure); var client = new Article.ArticleService.ArticleServiceClient(channel); var request = new SaveArticleRequest { Url = a.Url, Text = a.Text, Key = a.Key, Title = a.Title, Intro = a.Intro }; var reply = await client.SaveArticleAsync(request); await channel.ShutdownAsync(); return reply; }

In order to call the remote procedure I have to set up a channel and create a client object that will give me the function to call. In this implementation I am re-creating the channel for every call, but there might be a more optimal way to handle this. I would appreciate any feedback on better patterns here.