Conceitos básicos de MongoDB utilizando C#/.NET | Fazendo seu primeiro CRUD
Tags: dotnet, csharp, mongodb
Published: 2023-12-04 01:45:45

O MongoDB é um banco de dados não relacional, também chamado de **NoSQL**. É orientado a **documentos**, os quais podem ser descritos com dados no formato chave-valor utilizando o formato **JSON (JavaScript Object Notation)**.

> Os bancos de dados NoSQL, diferente dos bancos relacionais, não utilizam os esquemas de organização por meio de tabelas, linhas e colunas. E apresentam algumas vantagens como **escalabilidade**, **flexibilidade**, **bom desempenho** e **facilidade para consultas**.

---

### **Driver MongoDB C#**
Neste exemplo criaremos uma API Web que fará as operações de _CRUD (criar, ler, atualizar e deletar)_ no banco de dados NoSQL MongoDB. 

#### Modelo de configuração

- [Estação de trabalho] Instalar o MongoDB utilizando Docker:  [MongoDb Docker](https://balta.io/blog/mongodb-docker)

- [Código] Adicionar a dependência do pacote NuGet do MongoDB: [MongoDB.Driver](https://www.nuget.org/packages/MongoDB.Driver).

- [Código] Inicialmente adicionaremos os **valores de conexão** do banco de dados no arquivo de configurações denominando `appsettings.json`. Passando informações como a **string de conexão** (_ConnectionString_), o banco de dados (_DatabaseName_) e a coleção (_CollectionName_) que iremos acessar no MongoDB.

#### appsettings.json
```c#
{
    "MongoDatabase": {
        "ConnectionString": "mongodb://localhost:27017",
        "DatabaseName": "BookStore",
        "CollectionName": "Books"
    },
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning"
        }
    },
    "AllowedHosts": "*"
}
```

- [Código] Agora criaremos uma classe denominada `DatabaseSettings` que irá conter os parâmetros **equivalentes** aos que criamos no `appsettings.json`. Esta classe irá **armazenar os valores de propriedade** do `appsettings.json`.

```c#
public class DatabaseSettings
{
    public string ConnectionString { get; set; } = null!;

    public string DatabaseName { get; set; } = null!;

    public string CollectionName { get; set; } = null!;
}
```

- [Código] No `Program.cs` adicionaremos a linha a seguir. A instância de configuração da seção `MongoDatabase` presente no arquivo `appsettings.json` é registrada no contêiner de injeção de dependência.

```c#
...
builder.Services.Configure<DatabaseSettings>(
    builder.Configuration.GetSection("MongoDatabase"));
```

Em outras palavras, a propriedade `ConnectionString` de um objeto do tipo `DatabaseSettings` será populada com a propriedade `MongoDatabase:ConnectionString` do `appsettings.json`. 

---

#### Modelo de entidade

- [Código] A seguir criaremos nosso modelo de entidade que utilizaremos em todas as operações do CRUD.

```c#
public class Book
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    public string? Id { get; set; }

    [BsonElement("Name")]
    public string BookName { get; set; } = null!;

    public decimal Price { get; set; }

    public string Category { get; set; } = null!;

    public string Author { get; set; } = null!;
}
```

_Descrição das annotations:_
**[BsonId]**: Designa a propriedade Id como chave primária.
**[BsonRepresentation(BsonType.ObjectId)]**: Permite a passagem do parâmetro como tipo `string` em vez de uma estrutura `ObjectId`. O MongoDB processa a conversão de `string` para `ObjectId`.
**[BsonElement("Name")]**: O valor `Name`do atributo `[BsonElement]` representa o nome da propriedade da coleção do MongoDB.

---

#### Adicionando o serviço de operações CRUD

- [Código]: Criar uma classe de serviço denominada `BooksService`.

```c#
      public class BooksService
   {
       private readonly IMongoCollection<Book> _booksCollection;

       public BooksService(IOptions<DatabaseSettings> bookStoreDatabaseSettings)
       {
           var mongoClient = new MongoClient(
               bookStoreDatabaseSettings.Value.ConnectionString);

           var mongoDatabase = mongoClient.GetDatabase(
               bookStoreDatabaseSettings.Value.DatabaseName);

           _booksCollection = mongoDatabase.GetCollection<Book>(
               bookStoreDatabaseSettings.Value.CollectionName);
       }
   ...
```

No código acima temos nossa classe `BooksService` e seu construtor. Uma instância do `DatabaseSettings` é recuperada da injeção de dependência pela injeção do construtor. Utilizamos a interface `IOptions` pois já está registrado como **Singleton** e pode ser injetado em qualquer vida útil do serviço.

> _Em cenários onde as opções devem ser recalculadas a cada solicitação, podemos utilizar o `IOptionsSnapshot`. Sendo este registrado como **Scoped**._

Esta técnica fornece acesso para os valores de configuração do `appsettings.json`, que foi configurado no início deste artigo. 

- [Código]: Agora no **Program** adicionaremos a linha subsequente.

```c#
...
builder.Services.AddSingleton<BooksService>();
```

Neste trecho a classe `BooksService` é registrada com a injeção de dependência para dar suporte à injeção de construtor nas classes consumidoras. 

> O tempo de vida **Singleton** é o mais apropriado pois a classe `BooksService` utiliza uma dependência direta de `MongoClient` e de acordo com as **diretrizes oficiais do MongoDB**, recomenda-se armazenar uma instância do `MongoClient` com o tipo de vida útil Singleton.

> **Singleton** significa que um objeto do serviço é criado e fornecido para todas as requisições. Assim, todas as requisições obtém o mesmo objeto.

**Voltando** à classe `BooksService`, utilizamos os seguintes membros do `MongoDB.Driver`.

```c#
var mongoClient = new MongoClient(
        bookStoreDatabaseSettings.Value.ConnectionString);

var mongoDatabase = mongoClient.GetDatabase(
    bookStoreDatabaseSettings.Value.DatabaseName);

_booksCollection = mongoDatabase.GetCollection<Book>(
    bookStoreDatabaseSettings.Value.CollectionName);
```

**MongoClient** lê a instância do servidor para executar operações de banco de dados e espera uma string de conexão como parâmetro.

Após o `MongoClient` se conectar a uma instância do MongoDB, o método **GetDatabase** é utilizado para acessar um banco de dados.

Já o método **GetCollection** é utilizado para acessar uma coleção. Na chamada para o `GetCollection<TDocument>(collection)` temos o `TDocument` sendo o tipo de objeto armazenado na coleção e o `collection` representando o nome da coleção. 

- [Código]: Adicionaremos os seguintes métodos na nossa classe `BooksService`.

```c#
public class BooksService
{
 ...
    public async Task<List<Book>> GetAsync() =>
    await _booksCollection.Find(_ => true).ToListAsync();

    public async Task<Book?> GetAsync(string id) =>
        await _booksCollection.Find(x => x.Id == id).FirstOrDefaultAsync();

    public async Task CreateAsync(Book newBook) =>
        await _booksCollection.InsertOneAsync(newBook);

    public async Task UpdateAsync(string id, Book updatedBook) =>
        await _booksCollection.ReplaceOneAsync(x => x.Id == id, updatedBook);

    public async Task RemoveAsync(string id) =>
        await _booksCollection.DeleteOneAsync(x => x.Id == id);
}
```

Os seguintes métodos são acessados nesta classe:
**Find<TDocument>**: retorna todos os documentos na coleção que correspondem aos critérios de pesquisa fornecidos.
**InsertOneAsync**: insere o objeto fornecido como um novo documento na coleção.
**ReplaceOneAsync**: substitui o único documento que corresponde aos critérios de pesquisa fornecidos com o objeto fornecido.
**DeleteOneAsync**: exclui um único documento que corresponde aos critérios de pesquisa fornecidos.

> _Outros métodos que podem ser utilizados estão listados no link [IMongoCollectionExtensions Methods](https://mongodb.github.io/mongo-csharp-driver/2.14/apidocs/html/Methods_T_MongoDB_Driver_IMongoCollectionExtensions.htm)._

---

#### Adicionar um _controller_

- [Código]: No nosso último passo iremos adicionar um controlador utilizando o seguinte código. 

Neste trecho temos métodos de ação `GET`, `POST`, `PUT` e `DELETE`, que darão suporte às nossas solicitações. 

```c#
 [ApiController]
 [Route("api/[controller]")]
 public class BooksController : ControllerBase
 {
     private readonly BooksService _booksService;

     public BooksController(BooksService booksService) =>
         _booksService = booksService;

     [ProducesResponseType(404)]
     [ProducesResponseType(200)]
     [HttpGet]
     public async Task<List<Book>> Get() =>
     await _booksService.GetAsync();

     [ProducesResponseType(404)]
     [ProducesResponseType(200)]
     [HttpGet("{id:length(24)}")]
     public async Task<ActionResult<Book>> Get(string id)
     {
         var book = await _booksService.GetAsync(id);

         if (book is null)
         {
             return NotFound();
         }

         return book;
     }

     [ProducesResponseType(404)]
     [ProducesResponseType(200)]
     [HttpPost]
     public async Task<IActionResult> Post(Book newBook)
     {
         await _booksService.CreateAsync(newBook);

         return CreatedAtAction(nameof(Get), new { id = newBook.Id }, newBook);
     }

     [ProducesResponseType(404)]
     [ProducesResponseType(200)]
     [HttpPut("{id:length(24)}")]
     public async Task<IActionResult> Update(string id, Book updatedBook)
     {
         var book = await _booksService.GetAsync(id);

         if (book is null)
         {
             return NotFound();
         }

         updatedBook.Id = book.Id;

         await _booksService.UpdateAsync(id, updatedBook);

         return NoContent();
     }

     [ProducesResponseType(404)]
     [ProducesResponseType(200)]
     [HttpDelete("{id:length(24)}")]
     public async Task<IActionResult> Delete(string id)
     {
         var book = await _booksService.GetAsync(id);

         if (book is null)
         {
             return NotFound();
         }

         await _booksService.RemoveAsync(id);

         return NoContent();
     }
 }
```

Após isso, iremos adicionar a chamada do método `AddControllers` e para o método `MapControllers` no nosso **Program**

```c#
...
builder.Services.AddControllers()
    .AddJsonOptions(
        options => options.JsonSerializerOptions.PropertyNamingPolicy = null);
...
app.MapControllers();
```

> Com a alteração `AddJsonOptions` anterior, os nomes de propriedade na resposta JSON serializada da API Web correspondem aos respectivos nomes de propriedade no tipo de objeto.

---

**Ferramentas utilizadas**
- Pacote Nuget: [MongoDB.Driver](https://www.nuget.org/packages/MongoDB.Driver)
- Linguagem: C# .NET 8.0

**Código no GitHub**
- [Larissa Tavares - MongoDBSolutions](https://github.com/larissatavares-ads/MongoDBSolutions/tree/main/CrudBasicoMongoDB)

---

> _Com isso finalizamos nossos entendimentos iniciais acerca de MongoDB e sobre como configurá-lo de maneira adequada. Além de termos bastante aparato teórico para entender muito sobre o nosso código. O código final está disponível no GitHub e seu link está disponível acima. Abraços!_

---

**Referências**
[Treinaweb - O que é MongoDB?](https://www.treinaweb.com.br/blog/o-que-e-mongodb)
[Microsoft Learn - Criar uma API Web com o ASP.NET Core e o MongoDB](https://learn.microsoft.com/pt-br/aspnet/core/tutorials/first-mongo-app?view=aspnetcore-8.0&tabs=visual-studio)
[MongoDB - Connection String](https://mongodb.github.io/mongo-csharp-driver/2.14/reference/driver/connecting/#re-use)
[MongoDB - Databases and Collections](https://mongodb.github.io/mongo-java-driver/3.4/driver/tutorials/databases-collections/#:~:text=Once%20you%20have%20a%20MongoClient,store%20data%20for%20that%20database.)
[MongoDB - IMongoCollectionExtensions Methods](https://mongodb.github.io/mongo-csharp-driver/2.14/apidocs/html/Methods_T_MongoDB_Driver_IMongoCollectionExtensions.htm)