Explore o novo chatbot do Developer Center! O MongoDB AI chatbot pode ser acessado na parte superior da sua navegação para responder a todas as suas perguntas sobre o MongoDB .

Junte-se a nós no Amazon Web Services re:Invent 2024! Saiba como usar o MongoDB para casos de uso de AI .
Desenvolvedor do MongoDB
Central de desenvolvedor do MongoDBchevron-right
Idiomaschevron-right
C#chevron-right

Unindo coleções no MongoDB com .NET Core e um pipeline de agregação

Nic Raboy7 min read • Published Feb 16, 2022 • Updated Feb 03, 2023
MongoDBFramework de agregaçãoC#
Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Se você está acompanhando minha série .NET Core no MongoDB, deve se lembrar que exploramos a criação de um aplicativo de console simples, bem como de uma API RESTful com suporte básico a CRUD. Em ambos os exemplos, usamos filtros básicos ao interagir com o MongoDB a partir de nossos aplicativos.
Mas e se precisarmos fazer algo um pouco mais complexo, como unir dados de duas coleções diferentes do MongoDB?
Neste tutorial, vamos dar uma olhada nos pipelines de agregação e em algumas maneiras de trabalhar com eles em um aplicativo .NET Core.

Os requisitos

Antes de começarmos, existem alguns requisitos que devem ser atendidos para ser bem-sucedido:
  • Tenha um cluster do Atlas MongoDB implantado e configurado.
  • Instale o .NET Core 6+.
  • Instale os conjuntos de dados de amostra do MongoDB.
Usaremos o .NET Core 6.0 para este tutorial específico. Versões mais antigas ou mais recentes podem funcionar, mas há uma chance de que alguns dos comandos sejam um pouco diferentes. A expectativa é que você já tenha um cluster do Atlas pronto para ser usado. Esse pode ser um cluster M0 gratuito ou melhor, mas você precisará configurá-lo adequadamente com funções de usuário e regras de acesso à rede. Você também precisará anexar os conjuntos de dados de amostra do MongoDB.
Se precisar de ajuda com isso, confira um tutorial anterior que escrevi sobre o assunto.

Uma análise mais detalhada do modelo de dados e dos resultados esperados

Como esperamos realizar algumas coisas bastante complicadas neste tutorial, é provavelmente uma boa ideia dividir os dados que entram nele e os dados que esperamos que saiam dele.
Neste tutorial, usaremos o banco de dados sample_mflix e a coleção filmes. Também usaremos uma coleção de playlists personalizadas que adicionaremos ao banco de dados sample_mflix.
Para que você tenha uma ideia dos dados com os quais trabalharemos, pegue o seguinte documento da coleção de filmes:
1{
2 "_id": ObjectId("573a1390f29313caabcd4135"),
3 "title": "Blacksmith Scene",
4 "plot": "Three men hammer on an anvil and pass a bottle of beer around.",
5 "year": 1893,
6 // ...
7}
Tudo bem, não incluí o documento inteiro porque ele é muito grande. Conhecer todos os campos não vai ajudar ou prejudicar o exemplo, desde que estejamos familiarizados com o campo _id.
Agora, vejamos um documento na coleção playlist proposta:
1{
2 "_id": ObjectId("61d8bb5e2d5fe0c2b8a1007d"),
3 "username": "nraboy",
4 "items": [
5 "573a1390f29313caabcd42e8",
6 "573a1391f29313caabcd8a82"
7 ]
8}
Conhecer os campos no documento acima é importante, pois eles serão usados em todos os nossos pipelines de agregação.
Um dos aspectos mais importantes a serem observados entre as duas coleções é o fato de que os campos _id são ObjectId e os valores no campo items são strings. Falaremos mais sobre isso à medida que avançarmos.
Agora que conhecemos nossos documentos de entrada, vamos dar uma olhada no que esperamos como resultado de nossas queries. Se eu fosse fazer query de uma playlist, não ia querer os valores de id para cada um dos filmes. Quero que eles sejam totalmente expandidos, como os seguintes:
1{
2 "_id": ObjectId("61d8bb5e2d5fe0c2b8a1007d"),
3 "username": "nraboy",
4 "items": [
5 {
6 "_id": ObjectId("573a1390f29313caabcd4135"),
7 "title": "Blacksmith Scene",
8 "plot": "Three men hammer on an anvil and pass a bottle of beer around.",
9 "year": 1893,
10 // ...
11 },
12 {
13 "_id": ObjectId("573a1391f29313caabcd8a82"),
14 "title": "The Terminator",
15 "plot": "A movie about some killer robots.",
16 "year": 1984,
17 // ...
18 }
19 ]
20}
É aqui que os pipelines de agregação entram e alguns se juntam, porque não podemos simplesmente fazer um filtro normal em uma operação Find, a menos que queiramos realizar várias operaçõesFind.

Criando um novo aplicativo de console .NET Core com suporte ao MongoDB

Para manter as coisas simples, vamos criar um aplicativo de console que usa nosso pipeline de agregação. Você pode usar a lógica e aplicá-la a um aplicativo web, se for esse o seu interesse.
Na CLI, execute o seguinte:
1dotnet new console -o MongoExample
2cd MongoExample
3dotnet add package MongoDB.Driver
Os comandos acima criarão um novo projeto .NET Core e instalarão o driver MongoDB mais recente para C#. Tudo o que fizermos a seguir acontecerá no arquivo "Program.cs" do projeto
Abra o arquivo "Program.cs" e adicione o seguinte código C#:
1using MongoDB.Driver;
2using MongoDB.Bson;
3
4MongoClient client = new MongoClient("ATLAS_URI_HERE");
5
6IMongoCollection<BsonDocument> playlistCollection = client.GetDatabase("sample_mflix").GetCollection<BsonDocument>("playlist");
7
8List<BsonDocument> results = playlistCollection.Find(new BsonDocument()).ToList();
9
10foreach(BsonDocument result in results) {
11 Console.WriteLine(result["username"] + ": " + string.Join(", ", result["items"]));
12}
O código acima se conectará a um MongoDB cluster, obterá uma referência à nossa coleção de playlists e despejará todos os documentos dessa coleção no console. Encontrar e retornar todos os documentos da coleção não é um requisito para o pipeline de agregação, mas pode ajudar no processo de aprendizado.
A string ATLAS_URI_HERE pode ser obtida no MongoDB Atlas Dashboard após clicar em "Conectar" para um cluster específico.

Criação de um pipeline de agregação com o .NET Core usando estágios de BsonDocument bruto

Vamos explorar algumas opções diferentes para criar uma query de pipeline de agregação com o .NET Core. A primeira usará dados brutos do tipoBsonDocument.
Conhecemos nossos dados de entrada e sabemos nosso resultado esperado, então precisamos criar algumas etapas de pipeline para reuni-los.
Vamos começar com o primeiro estágio:
1BsonDocument pipelineStage1 = new BsonDocument{
2 {
3 "$match", new BsonDocument{
4 { "username", "nraboy" }
5 }
6 }
7};
O primeiro estágio desse pipeline usa o operador $match para encontrar apenas documentos em que o username é "nraboy". Pode ser mais de um porque não estamos tratando username como um campo único.
Com o filtro em vigor, vamos para o próximo estágio:
1BsonDocument pipelineStage2 = new BsonDocument{
2 {
3 "$project", new BsonDocument{
4 { "_id", 1 },
5 { "username", 1 },
6 {
7 "items", new BsonDocument{
8 {
9 "$map", new BsonDocument{
10 { "input", "$items" },
11 { "as", "item" },
12 {
13 "in", new BsonDocument{
14 {
15 "$convert", new BsonDocument{
16 { "input", "$$item" },
17 { "to", "objectId" }
18 }
19 }
20 }
21 }
22 }
23 }
24 }
25 }
26 }
27 }
28};
Lembra como os campos do documento _id eram ObjectId e o array items eram strings? Para que a junção seja bem-sucedida, eles precisam ser do mesmo tipo. O segundo estágio do pipeline é mais um estágio de manipulação com o operador $project . Estamos definindo os campos que queremos passar para o próximo estágio, mas também estamos modificando alguns dos campos, em particular o campo items . Usando o operador $map podemos pegar os valores da string e convertê-los em valores ObjectId.
Se sua array items contivesse ObjectId em vez de valores de string, esse estágio específico não seria necessário. Também pode não ser necessário se você estiver usando classes POCO em vez de tipos BsonDocument. Essa é uma lição para outro dia.
Com os valores dos itens mapeados corretamente, podemos enviá-los para o próximo estágio do pipeline:
1BsonDocument pipelineStage3 = new BsonDocument{
2 {
3 "$lookup", new BsonDocument{
4 { "from", "movies" },
5 { "localField", "items" },
6 { "foreignField", "_id" },
7 { "as", "movies" }
8 }
9 }
10};
O estágio do pipeline acima é onde a operação JOIN realmente acontece. Estamos analisando a coleção de filmes e estamos usando os campos ObjectId de nossa coleção de playlists para uni-los ao campo _id de nossa coleção de filmes. A saída deste JOIN será armazenada em um novo campo movies.
O $lookup é como dizer o seguinte:
1SELECT movies
2FROM playlist
3JOIN movies ON playlist.items = movies._id
É claro que há mais do que a declaração SQL acima porque items é uma array, algo com o qual você não pode trabalhar nativamente na maioria dos bancos de dados SQL.
Então, a partir de agora, temos nossos dados agregados. No entanto, não é tão elegante quanto o que queríamos em nosso resultado final. Isso ocorre porque a saída $lookup é um array que nos deixará com uma matriz multidimensional. Lembre-se, items era um array e cada movies é um array. Não é a coisa mais agradável de se trabalhar, então provavelmente vamos manipular ainda mais os dados em outro estágio.
1BsonDocument pipelineStage4 = new BsonDocument{
2 { "$unwind", "$movies" }
3};
O estágio acima pegará nosso novo campo movies e o nivelará com o operador $unwind. O operador $unwind basicamente pega cada elemento de uma array e cria um novo item de resultado para ficar adjacente ao restante dos campos do documento pai. Se você tiver, por exemplo, um documento que possui um array com dois elementos, depois de fazer um $unwind, terá dois documentos.
Nosso objetivo final, no entanto, é terminar com um array de filmes de dimensão única, para que possamos corrigir isso com outro estágio do pipeline.
1BsonDocument pipelineStage5 = new BsonDocument{
2 {
3 "$group", new BsonDocument{
4 { "_id", "$_id" },
5 {
6 "username", new BsonDocument{
7 { "$first", "$username" }
8 }
9 },
10 {
11 "movies", new BsonDocument{
12 { "$addToSet", "$movies" }
13 }
14 }
15 }
16 }
17};
O estágio acima agrupará nossos documentos e adicionará nossos filmes não processados a um novo campo movies, que não é multidimensional.
Então, vamos reunir os estágios do pipeline para que possam ser executados em nosso aplicativo.
1BsonDocument[] pipeline = new BsonDocument[] {
2 pipelineStage1,
3 pipelineStage2,
4 pipelineStage3,
5 pipelineStage4,
6 pipelineStage5
7};
8
9List<BsonDocument> pResults = playlistCollection.Aggregate<BsonDocument>(pipeline).ToList();
10
11foreach(BsonDocument pResult in pResults) {
12 Console.WriteLine(pResult);
13}
Executar o código até agora deve nos dar o resultado esperado em termos de dados e formato.
Agora, você pode estar pensando que o pipeline de cinco estágios acima foi muito trabalhoso para uma operação JOIN. Há alguns aspectos dos quais você deve estar ciente:
  • Nossos valores de id não eram do mesmo tipo, o que resultou em outro estágio.
  • Nossos valores a serem unidos estavam em uma array, e não em um relacionamento de um para um.
O que estou tentando dizer é que o tamanho e a complexidade do seu pipeline dependerão de como você escolheu modelar seus dados.

Usando uma API Fluent para criar estágios de pipeline de agregação

Vamos dar uma olhada em outra maneira de alcançar o resultado desejado. Podemos usar a API Fluent que o MongoDB oferece em vez de criar um array de estágios de pipeline.
Dê uma olhada no seguinte:
1var pResults = playlistCollection.Aggregate()
2 .Match(new BsonDocument{{ "username", "nraboy" }})
3 .Project(new BsonDocument{
4 { "_id", 1 },
5 { "username", 1 },
6 {
7 "items", new BsonDocument{
8 {
9 "$map", new BsonDocument{
10 { "input", "$items" },
11 { "as", "item" },
12 {
13 "in", new BsonDocument{
14 {
15 "$convert", new BsonDocument{
16 { "input", "$$item" },
17 { "to", "objectId" }
18 }
19 }
20 }
21 }
22 }
23 }
24 }
25 }
26 })
27 .Lookup("movies", "items", "_id", "movies")
28 .Unwind("movies")
29 .Group(new BsonDocument{
30 { "_id", "$_id" },
31 {
32 "username", new BsonDocument{
33 { "$first", "$username" }
34 }
35 },
36 {
37 "movies", new BsonDocument{
38 { "$addToSet", "$movies" }
39 }
40 }
41 })
42 .ToList();
43
44foreach(var pResult in pResults) {
45 Console.WriteLine(pResult);
46}
No exemplo acima, usamos métodos como Match, Project, Lookup, Unwind e Group para obter o resultado final. Para alguns desses métodos, não foi necessário usar BsonDocument como vimos no exemplo anterior.

Conclusão

Você acabou de ver duas maneiras de fazer um pipeline de agregação do MongoDB para unir coleções em um aplicativo .NET Core. Como mencionado anteriormente, há algumas maneiras de realizar o que queremos, e todas elas dependerão de como você escolheu modelar os dados em suas coleções.
Há uma terceira maneira, que exploraremos em outro tutorial, e ela usa o LINQ para realizar o trabalho.
Se tiver dúvidas sobre algo que viu neste tutorial, visite os Fóruns MongoDB Community e deixe-se envolver!

Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Relacionado
Tutorial

Criando um jogo de captura de espaço no Unity que sincroniza com o Realm e o MongoDB Atlas


Jun 26, 2024 | 24 min read
Artigo

Como usar o Realm de forma eficaz em um aplicativo Xamarin.Forms


Sep 09, 2024 | 18 min read
Tutorial

Trabalhando com transações MongoDB com C# e .NET Framework


Sep 11, 2024 | 3 min read
Artigo

MongoDB Provider para EF Core: As atualizações mais recentes


Aug 29, 2024 | 6 min read
Sumário