gRPC+ MongoDb + Docker + .net Core 3.1(pt)

Bernardo Teixeira
9 min readFeb 23, 2020

--

Primeiro, uma breve explicação sobre este projeto. Este projeto é um CRUD simples para um aplicativo de tarefas.

Usaremos o .net core 3.1, MongoDb como base de dado, conteiners para a Basse de dados (MongoDb) e o gRPC para a comunicação entre cliente e servidor. Também usamos o Swagger para fazer o nosso “Front-End”.

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

O que é gRPC?

O gRPC é uma estrutura para conectar com eficiência serviços e criar sistemas distribuídos. Inicialmente desenvolvido pelo Google, agora é um projeto de código aberto que promove o modelo RPC (Remote Procedure Call) para comunicação entre serviços. É focado no alto desempenho e usa o protocolo HTTP / 2 para transportar mensagens binárias.

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

O que é o MongoDb?

O MongoDB é uma base de dados. Classificado como uma base de daods NoSQL, o MongoDB usa documentos semelhantes a JSON com esquema.

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

O que é o Docker?

O Docker é um conjunto de produtos de plataforma como serviço (PaaS) que usam a virtualização no nível do SO para fornecer software em pacotes chamados contêineres. Os contêineres são isolados um do outro e agrupam seu próprio software, bibliotecas e arquivos de configuração.

Como começar?

Abra seu Visual Studio (eu uso o Visual Studio 2019), crie um projeto Web. Na solução cria outro projeto Web.
Um será o cliente e outro o servidor.
O cliente é o local onde o Swagger estará, para que possamos fazer solicitações e o servidor esta conectado á base de dados.

Client Side

No projecto do Cliente intalle os seguintes NuGets Packages:

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

Vamos começar pelo Swagger, para isso vamos ao ficheiro Startup.cs e escrevemos:

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(); });}

Agora você tem o Swagger, mas, se você iniciar o seu cliente, o URL estará incorreto. Então, para isso, vá ao Client Properties, Debug tab and in Lauch browser and type -> swagger.

Cria uma pasta com o nome Model, e cria o ficheiro Todo.cs, esta classe será mapeada entre o Swagger e o gRPC e vice-versa.

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

gRPC Client

Cria a pastas Protos. E cria o ficheiro 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{}

O que está acontecer aqui?

Você está criando um serviço chamado Todo. e você tem quatro métodos, GetTodos, CreateTodo, DeleteTodo e SingleTodo.

E cada método tem uma mensagem, uma mensagem é um agregado para os parâmetros que queres enviar ou receber. Cada mensagem possui diferentes tipos de parâmetros. Você também tem que dizer qual é o retorno. Nesse caso, quando enviamos um GetTodos, logicamente não precisamos enviar nada, mas o gRPC obriga a enviar uma mensagem, para criar uma mensagem vazia, basta ver a mensagem VoidRequest. Eu acho que é a maneira mais fácil de criar uma mensagem nula.

Um em particular, é returns(stream TodoReturn); no gRPC para receber ou enviar uma lista, poderíamos usar stream ou repeated. Qual é a diferença?

Stream vs Repeated

Repeated, requer que todas as mensagens sejam preparadas pelo servidor antes de serem enviadas e que todo o conjunto de mensagens seja recebido pelo cliente antes de qualquer processamento, aumentando a latência.

Stream, permite que o cliente processe as mensagens recebidas uma de cada vez.

Agora clique com o botão direito do rato e vá para as Properties, é preciso alterar o Build Action para ProtoBuf compiler. E tambem mudar o Grpc Stub Classes para Client only. Nós poderíamos fazer cliente e servidor. Mas eu prefiro essa maneira de explicar o que está acontecendo no servidor e no cliente. Agora faça um Rebuild no projecto client.

Porque é preciso dar Rebuild ao projecto?

Porque o Protobuf vai gerar o código do todo.proto.

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

Agora é preciso criar um pasta, Controllers, e dentro dessa pasta cria o ficheiro TodoController.cs.

Precisamos obter todas as tarefas, precisamos, criar uma tarefa e excluir uma tarefa. E também precisamos procurar uma tarefa, por nome.

Eu criei o metodo client() para abrir o channel e assim conectar o client ao server.

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

O Todo.TodoClient é gerado pelo ProtoBuf compiler.

Obter todos as tarefas

[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;}

Crie um método Get para nos fornecer todas as tarefas. Chame o método client () para criar o canal e, em seguida, o método GetTodoByName. Criamos esse método no todos.protos.

Também usamos um ResponseStream para obter todas as tarefas, provenientes do servidor. O MoveNext () é apenas para descobrir se o servidor está enviando outras tarefas.

Obter apenas uma tarefa

[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;}

Observe que aqui não usamos o ResponseStream. Por quê? Porque estamos apenas esperando uma tarefa. Idealmente, você deve procurar o ID da tarefa, mas por uma questão de explicação, é mais fácil ver o que está acontecendo dessa maneira.

Criar uma tarefa

[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;}

Para criar uma tarefa, temos apenas a mesma lógica chamada client () e o método CreateTodoAsync, gRPC oferece duas maneiras, assíncrona e sincronizada.

Vê mais aqui https://medium.com/@bernardo.teixeira.691/sync-async-multi-thread-12caca3074f9

Excluir uma tarefa

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

return result.Bool;
}

Para excluir uma tarefa, como eu disse, uso o nome da tarefa para fins de explicação. Envie o nome para o servidor.

E aguarde um bool para saber se o resultado falhou ou foi bem-sucedido. Este bool é a resposta do servidor.

Server Side

Crie um aplicativo Web no visual studio, chamei GrpcSample. Precisa instalar alguns NuGet packages.

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

gRPC Server

Podemos copiar o lado do cliente. Então crie uma pasta Protos, dentro de crie um ficheiro todo.proto. E cole o código do lado do cliente.

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{}

Se copiar o lado do cliente, esteja ciente de que precisa alterar o csharp_namespace para o lado do servidor. Não se esqueça, Build o projeto do servidor.

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; }}

O BsonId é uma anotação para o MongoDb e o ObjectId é para indicar o ID do documento do Mongo.

MongoDb

Começamos com appsettings.json, onde precisamos especificar o Container do Mongo.

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

A base de dados será TodoDB e a porta 27017. Poderíamos escrever o user e a senha, e sugiro que você faça isso, mas pela explicação está tudo bem.

Crie uma pasta MongoDB, aqui temos toda a configuração necessária para o mongoDb. Precisamos criar o contexto para o MongoDb. Crie uma classe, TodoContext.cs e ITodoContext.cs como 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; }}

Agora precisamos da classe a para obter as propriedades de appsettings.json. Para isso, criamos a classe 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}";        }    }}

Criamos tambem a classe, ServerConfig.cs .

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

Repository

Depois disso, crie uma pasta Repository e aqui crie uma interface e uma classe. Para a classe, vou nomear TodoRepository.cs e para a 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

Crie uma pasta, Services, dentro cria a classe Todoservice.cs. Aqui está a lógica do servidor.
Primeiro, precisamos implementar o Todo.TodoBase, que é gerado pelo protocol buffer compiler.

public class TodoService : Todo.TodoBase

Dessa maneira, podemos substituir os métodos RPC.

Usaremos a injeção de dependência, para nos dar acesso ao TodoRepository.cs.

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

A classe inteira

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); }}}

Esta quase!

Agora, basta registrar tudo na classe Startup.cs.

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);}

Precisamos registrar a configuração do gRPC, MongoDb e o repositório.

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

Depois de instalar o Docker. Vá, ao PowerShell.

Escreva :

docker pull mongo

Para baixar a imagem do Mongo, para iniciar o contêiner com essa imagem.

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

Para verificar se o contêiner está executando, escreva:

docker container ls

Run the solution

Propriedades da solução, projeto de inicialização. Selecione a opção Projeto de inicialização múltipla.

Escolha as opções de início nos dois projetos, Servidor e Cliente.

E é isso.

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

No responses yet