gRPC+ MongoDb + Docker + .net Core 3.1(pt)
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