gRPC+ MongoDb + Docker + .net Core 3.1

Bernardo Teixeira
9 min readFeb 19, 2020

--

First a brief explanation about this project. This project is a simple CRUD for a To-do application.

We will use .net core 3.1, MongoDb as Database, docker to containerize the Database (MongoDb) and gRPC for the communication between client and server. We also use Swagger to make our “Front-End”.

GitHub: https://github.com/bteixeira691/GrpcSample

What is gRPC?

gRPC is a framework to efficiently connect services and build distributed systems. Initially designed by Google, it is now an open-source project promoting the Remote Procedure Call (RPC) model for communication between services. It is focused on high performance and uses the HTTP/2 protocol to transport binary messages.

https://medium.com/red-crane/grpc-and-why-it-can-save-you-development-time-436168fd0cbc

What is MongoDb?

MongoDB is a cross-platform document-oriented database program. Classified as a NoSQL database program, MongoDB uses JSON-like documents with schema.

https://medium.com/@saivittalb/introduction-to-mongodb-859ed4426994

What is docker?

Docker is a set of platform as a service (PaaS) products that use OS-level virtualization to deliver software in packages called containers. Containers are isolated from one another and bundle their own software, libraries and configuration files.

How to start?

Open your Visual Studio (I use Visual Studio 2019), create a Web Project. In the Solution creates other Web Project.

One will be the Client and other the Server.

The client is where swagger will be, so we can make requests, and the server will connect the database.

Client Side

In client install the follow NuGets Packages:

  • Swashbuckle.AspNetCore 5.0.0
  • Google.Protobuf 3.11.2
  • Grpc.Net.Client 2.25.0

Let’s start will Swagger, go to Startup.cs and write this:

public void ConfigureServices(IServiceCollection services){    services.AddControllers();    services.AddSwaggerGen(c =>    {        c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API",       Version = "v1" });    });}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{ app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); }); app.UseHttpsRedirection(); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); });}

Now you have the Swagger but, if you start your Client the URL will be incorrect. So for this go to Client Properties, Debug tab and in Lauch browser and type -> swagger.

Create a folder name it Model, here you will put the file Todo.cs, this class will be mapped between swagger and gRPC and vice versa.

public class Todo{   public string Title { get; set; }   public string Content { get; set; }   public string Category { get; set; }}

gRPC Client

Create a folder name it Protos. Create a file todo.proto.

syntax = "proto3";option csharp_namespace = "GrpcClient";service Todo{rpc GetTodos(VoidRequest) returns (stream TodoReturn);rpc CreateTodo(TodoReturn) returns(returnBool);rpc DeleteTodo(GetTodoName) returns(returnBool);rpc SingleTodo(GetTodoName) returns(TodoReturn);}message GetTodoName {string name=1;}message TodoReturn {string title =1;string content =2;string category=3;}message returnBool {bool bool = 1;}message VoidRequest{}

What is happening where?

You are creating a service called Todo. and you have four methods, GetTodos, CreateTodo, DeleteTodo and SingleTodo.

And each method has a message, a message is an aggregate for parameters you have to send or receive. Each message has different type of parameters. You also have to say what is the return. In this case when we send a GetTodos, logically we don’t need to send anything, but gRPC forces you to send a message, to create an empty just look at message VoidRequest. I guess, it‘s the easier way to create a void message.

One in particular, is the returns (stream TodoReturn); in gRPC to receive or send a list we could use stream or repeated. What is the difference?

Stream vs Repeated

Repeated, requires all of the messages to be prepared by the server before any are sent, and the entire set of messages to be received by the client before doing any processing, increasing latency.

Stream, allow the client to process the incoming messages one at a time.

Now right click, and go to Properties, and you have to change the Build Action to ProtoBuf compiler. And change the Grpc Stub Classes to Client only. We could make Client and Server. But I prefer this way to explain what is happening in the server and the client. Now do a Rebuild in your Client application.

Why need to Rebuild the client project?

Because Protobuf will generate code from your todo.proto.

More detail -> https://medium.com/@philipshen13/a-short-introduction-to-grpc-419b620e2177

Now we need to create the controller, for this in Controllers folder, create a TodoController.cs.

We need to get all the To-dos, we need to create one To-do and we need to delete one To-do. And we also need to get a single To-do, by name.

I created a client() method, just to open the connection between client and server.

private Todo.TodoClient client(){    var channel = GrpcChannel.ForAddress("https://localhost:5001");    return new Todo.TodoClient(channel);}

The Todo.TodoClient is generated by ProtoBuf compiler.

Get All Todos

[HttpGet]public async Task<IList<Model.Todo>> GetAsync(){    List<Model.Todo> listTodo = new List<Model.Todo>();    using (var result = client().GetTodos(new VoidRequest()))    {       while (await result.ResponseStream.MoveNext())       {          var todo = new GrpcClient.Model.Todo          {             Category = result.ResponseStream.Current.Category,             Content = result.ResponseStream.Current.Content,             Title = result.ResponseStream.Current.Title          };          listTodo.Add(todo);       }    };    return listTodo;}

Create a Get method to give us all the to-dos. Call the client() method to create the channel, then GetTodoByName method. We created this method in todos.protos.

We also use a ResponseStream to get all the to-dos, coming from the server. The MoveNext() is just to find out if the server is sending other to-dos.

Get a Single Todo

[HttpGet("{name}", Name = "Get")]public async Task<Model.Todo> GetSingleTodo(string name){    var result = await client().SingleTodoAsync(new GetTodoName { Name = name });    var todo = new Model.Todo    {        Category = result.Category,       Content = result.Content,       Title = result.Title    };    return todo;}

Notice that here we don’t use the ResponseStream. Why? Because we are just waiting for one to-do. Ideally you should look for the to-do Id,but for the sake of explanation, it’s easier to see what’s going on this way.

Create a To-do

[HttpPost]public async Task<bool> Post(Model.Todo todo){    var result = await client().CreateTodoAsync(new TodoReturn    {       Category = todo.Category,       Content = todo.Content,       Title = todo.Title    });return result.Bool;}

To create a to-do, we just have the same logic call the client() and the method, CreateTodoAsync, gRPC offers two ways, async and sync.

See more here https://medium.com/@bernardo.teixeira.691/sync-async-multi-thread-12caca3074f9

Delete a To-do

public async Task<bool> Delete(string name)        
{
var result = await client().DeleteTodoAsync(new GetTodoName
{ Name = name });

return result.Bool;
}

To delete a to-do, as I said, I use the to-do name for explanation purpose. Send the name for the server.

And wait for a bool to know if the result is failed or success. This bool is the response from the server.

Server Side

Create a web application in visual studio, I called GrpcSample. Need to install some NuGet packages.

  • Google.Protobuf 3.11.2
  • Grpc.AspNetCore 2.24.0
  • MongoDB.Drvier 2.10.0

gRPC Server

We can copy the client side. So create a folder Protos, inside create a todo.proto file. And paste the client side code.

syntax = "proto3"; 
option csharp_namespace = "GrpcSample.Protos";
service Todo{
rpc GetTodos(VoidRequest) returns (stream TodoReturn);rpc CreateTodo(TodoReturn) returns(returnBool);rpc DeleteTodo(GetTodoName) returns(returnBool);rpc SingleTodo(GetTodoName) returns(TodoReturn);}message GetTodoName {string name=1;}message TodoReturn {string title =1;string content =2;string category=3;}message returnBool {bool bool = 1;}message VoidRequest{}

If you copy the client side be aware you need to change the csharp_namespace for your server side. Don’t forget, build the Server Project.

Todo Model

public class Todo{    [BsonId]    public ObjectId InternalId { get; set; }    public string Title { get; set; }    public string Content { get; set; }    public string Category { get; set; }}

The BsonId is an annotation for MongoDb and ObjectId is for indicate the ID for Mongo document.

MongoDb

We start with appsettings.json where we need to specify the Mongo Container.

{  
"MongoDB":
{
"Database": "TodoDB",
"Host": "localhost",
"Port": 27017
}
}

The database will be TodoDB and the port 27017. We could write the user and password, and I suggest you do that, but for the explanation it’s fine.

Create a MongoDB folder, here we have all the configuration needed for mongoDb. We need to create the context for MongoDb. Create a class, TodoContext.cs and ITodoContext.cs as interface.

TodoContext.cs

public class TodoContext : ITodoContext{    private readonly IMongoDatabase _db;    public TodoContext(MongoDBConfig config)    {        var client = new MongoClient(config.ConnectionString);        _db = client.GetDatabase(config.Database);    }
public IMongoCollection<Todo> Todos => _db.GetCollection<Todo>("Todos");}

ITodoContext.cs

public interface ITodoContext{    IMongoCollection<Todo> Todos { get; }}

Now we need the a class to get the properties from appsettings.json. For this, we created the class, MongoDBConfig.cs .

MongoDBConfig.cs

public class MongoDBConfig{    public string Database { get; set; }    public string Host { get; set; }    public int Port { get; set; }    public string User { get; set; }    public string Password { get; set; }    public string ConnectionString    {       get       {          if (string.IsNullOrEmpty(User) || string.IsNullOrEmpty(Password))         return $@"mongodb://{Host}:{Port}";         return $@"mongodb://{User}:{Password}@{Host}:{Port}";       }     }}

Create as well the class, ServerConfig.cs .

public class ServerConfig{    public MongoDBConfig MongoDB { get; set; } = new MongoDBConfig();}

Repository

After this, create a folder Repository and here create an interface and a class. For the class, I will name TodoRepository.cs and for the interface ITodoRepository.cs .

TodoRepository.cs

public class TodoRepository : ITodoRepository    
{
private readonly ITodoContext _context;
public TodoRepository(ITodoContext context)
{
_context = context;
}
public async Task<IEnumerable<Todo>> GetAllTodos()
{
return await _context.Todos.Find(_ => true).ToListAsync();
}

public async Task<IEnumerable<Todo>> GetTodo(string)
{
FilterDefinition<Todo> filter = Builders<Todo>.Filter.Eq(m => m.Title, name);

return await _context.Todos.Find(filter).ToListAsync();
}
public async Task Create(Todo todo)
{
await _context.Todos.InsertOneAsync(todo);
}

public async Task<bool> Delete(string name)
{
FilterDefinition<Todo> filter = Builders<Todo>.Filter.Eq(m => m.Title, name);

DeleteResult deleteResult = await _context.Todos.DeleteOneAsync(filter);
return deleteResult.IsAcknowledged&& deleteResult.DeletedCount > 0;
}
}

ITodoRepository.cs

public interface ITodoRepository{    Task<IEnumerable<Todo>> GetAllTodos();    Task<IEnumerable<Todo>> GetTodo(string name);    Task Create(Todo todo);    Task<bool> Delete(string name);}

Todo Service

Create a folder, Services, inside creates the class Todoservice.cs. Here is the server logic.

First, we need to implement the Todo.TodoBase this is Generated by the protocol buffer compiler.

public class TodoService : Todo.TodoBase

In this way we can override the RPC methods.

We will use Dependency Injection, to give us the access to the TodoRepository.cs .

private readonly ITodoRepository _toporepository;public TodoService(ITodoRepository todoRepository){    _toporepository = todoRepository;}

The whole class:

public class TodoService : Todo.TodoBase{    private readonly ITodoRepository _toporepository;    public TodoService(ITodoRepository todoRepository)    {        _toporepository = todoRepository;    }    public override async Task<returnBool> CreateTodo(TodoReturn request, ServerCallContext context)    {        try        {           await _toporepository.Create(new Model.Todo           {             Category = request.Category,             Content = request.Content,             Title = request.Title            }).ConfigureAwait(false);           var response = new returnBool() { Bool = true };           return await Task.FromResult(response);         }         catch (Exception e)         {           var response = new returnBool() { Bool = false };           return await Task.FromResult(response);         }
}
public override async Task GetTodos(VoidRequest request, IServerStreamWriter<TodoReturn> responseStream, ServerCallContext context) { List<TodoReturn> listTodo = new List<TodoReturn>(); var result = await _toporepository.GetAllTodos(); foreach (var item in result) { listTodo.Add(new TodoReturn { Category = item.Category, Content = item.Content, Title = item.Title }); } foreach (var item in listTodo) { await responseStream.WriteAsync(item); } } public override async Task<returnBool> DeleteTodo(GetTodoName request, ServerCallContext context) { try { var result = await _toporepository.Delete(request.Name).ConfigureAwait(false); var response = new returnBool() { Bool = result }; return await Task.FromResult(response); } catch (Exception e) { var response = new returnBool() { Bool = false }; return await Task.FromResult(response); } }}

Almost done!

Now just need to register everything in the Startup.cs file.

public void ConfigureServices(IServiceCollection services){    services.AddGrpc();    var config = new ServerConfig();    Configuration.Bind(config);    var todoContext = new TodoContext(config.MongoDB);    var repo = new TodoRepository(todoContext);    services.AddSingleton<ITodoRepository>(repo);}

We need to register the gRPC, MongoDb configuration and the Repository.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env){    if (env.IsDevelopment())    {        app.UseDeveloperExceptionPage();    }    app.UseRouting();    app.UseEndpoints(endpoints =>    {        endpoints.MapGrpcService<TodoService>();        endpoints.MapGet("/", async context =>        {           await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");        });    });}

Docker

After install docker desktop. Go go the PowerShell.

Write:

docker pull mongo

To download the Mongo image.Then we just need to start the container with that image.

docker run --name myMongoTestDb -p 27017:27017 -d mongo

To check if the container is running write:

docker container ls

Run the solution

Solution properties, startup project. Select the Multiple Startup Project option.

Choose the start options in both projects, Server and Client.

And that’s it.

Philip Shen Napon Mekavuthikul Sai Vittal B Microsoft + Open Source Google Developers

--

--

Bernardo Teixeira
Bernardo Teixeira

Written by Bernardo Teixeira

Software Engineer.. Red Panda Lover

Responses (1)