Совокупный поиск $ с С#

У меня есть следующий запрос MongoDb:

db.Entity.aggregate(
    [
        {
            "$match":{"Id": "12345"}
        },
        {
            "$lookup": {
                "from": "OtherCollection",
                "localField": "otherCollectionId",
                "foreignField": "Id",
                "as": "ent"
            }
        },
        { 
            "$project": { 
                "Name": 1,
                "Date": 1,
                "OtherObject": { "$arrayElemAt": [ "$ent", 0 ] } 
            }
        },
        { 
            "$sort": { 
                "OtherObject.Profile.Name": 1
            } 
        }
    ]
)

Это возвращает список объектов, соединенных с соответствующим объектом из другой коллекции.

Кто-нибудь знает, как я могу использовать это в С#, используя LINQ или используя эту точную строку?

Я попытался использовать следующий код, но он не может найти типы QueryDocument и MongoCursor - я думаю, что они устарели?

BsonDocument document = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<BsonDocument>("{ name : value }");
QueryDocument queryDoc = new QueryDocument(document);
MongoCursor toReturn = _connectionCollection.Find(queryDoc);

Ответ 1

Нет необходимости анализировать JSON. Здесь все можно сделать напрямую с помощью интерфейсов LINQ или Aggregate Fluent.

Просто с использованием некоторых демонстрационных классов, потому что вопрос на самом деле не дает много для продолжения.

Настроить

В основном у нас есть две коллекции здесь, будучи

юридические лица

{ "_id" : ObjectId("5b08ceb40a8a7614c70a5710"), "name" : "A" }
{ "_id" : ObjectId("5b08ceb40a8a7614c70a5711"), "name" : "B" }

и другие

{
        "_id" : ObjectId("5b08cef10a8a7614c70a5712"),
        "entity" : ObjectId("5b08ceb40a8a7614c70a5710"),
        "name" : "Sub-A"
}
{
        "_id" : ObjectId("5b08cefd0a8a7614c70a5713"),
        "entity" : ObjectId("5b08ceb40a8a7614c70a5711"),
        "name" : "Sub-B"
}

И пара классов, чтобы связать их, так же как очень простые примеры:

public class Entity
{
  public ObjectId id;
  public string name { get; set; }
}

public class Other
{
  public ObjectId id;
  public ObjectId entity { get; set; }
  public string name { get; set; }
}

public class EntityWithOthers
{
  public ObjectId id;
  public string name { get; set; }
  public IEnumerable<Other> others;
}

 public class EntityWithOther
{
  public ObjectId id;
  public string name { get; set; }
  public Other others;
}

Запросы

Свободный интерфейс

var listNames = new[] { "A", "B" };

var query = entities.Aggregate()
    .Match(p => listNames.Contains(p.name))
    .Lookup(
      foreignCollection: others,
      localField: e => e.id,
      foreignField: f => f.entity,
      @as: (EntityWithOthers eo) => eo.others
    )
    .Project(p => new { p.id, p.name, other = p.others.First() } )
    .Sort(new BsonDocument("other.name",-1))
    .ToList();

Запрос отправлен на сервер:

[
  { "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
  { "$lookup" : { 
    "from" : "others",
    "localField" : "_id",
    "foreignField" : "entity",
    "as" : "others"
  } }, 
  { "$project" : { 
    "id" : "$_id",
    "name" : "$name",
    "other" : { "$arrayElemAt" : [ "$others", 0 ] },
    "_id" : 0
  } },
  { "$sort" : { "other.name" : -1 } }
]

Вероятно, самый простой для понимания, поскольку свободный интерфейс в основном совпадает с общей структурой BSON. Этап $lookup имеет все те же аргументы, а $arrayElemAt представлен First(). Для $sort вы можете просто предоставить документ BSON или другое допустимое выражение.

Альтернативой является более новая выразительная форма $lookup с оператором sub-pipe для MongoDB 3.6 и выше.

BsonArray subpipeline = new BsonArray();

subpipeline.Add(
  new BsonDocument("$match",new BsonDocument(
    "$expr", new BsonDocument(
      "$eq", new BsonArray { "$$entity", "$entity" }  
    )
  ))
);

var lookup = new BsonDocument("$lookup",
  new BsonDocument("from", "others")
    .Add("let", new BsonDocument("entity", "$_id"))
    .Add("pipeline", subpipeline)
    .Add("as","others")
);

var query = entities.Aggregate()
  .Match(p => listNames.Contains(p.name))
  .AppendStage<EntityWithOthers>(lookup)
  .Unwind<EntityWithOthers, EntityWithOther>(p => p.others)
  .SortByDescending(p => p.others.name)
  .ToList();

Запрос отправлен на сервер:

[ 
  { "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
  { "$lookup" : {
    "from" : "others",
    "let" : { "entity" : "$_id" },
    "pipeline" : [
      { "$match" : { "$expr" : { "$eq" : [ "$$entity", "$entity" ] } } }
    ],
    "as" : "others"
  } },
  { "$unwind" : "$others" },
  { "$sort" : { "others.name" : -1 } }
]

Fluent "Builder" пока не поддерживает синтаксис напрямую, а выражения LINQ не поддерживают оператор $expr, однако вы все равно можете создавать с использованием BsonDocument и BsonArray или других допустимых выражений. Здесь мы также "печатаем" результат $unwind, чтобы применить $sort используя выражение, а не BsonDocument как показано ранее.

Помимо других применений, основная задача "субпроводника" - уменьшить количество документов, возвращаемых в целевом массиве $lookup. Кроме того, $unwind здесь служит для фактического "слияния" с оператором $lookup при выполнении сервера, так что это обычно более эффективно, чем просто захват первого элемента результирующего массива.

Queryable GroupJoin

var query = entities.AsQueryable()
    .Where(p => listNames.Contains(p.name))
    .GroupJoin(
      others.AsQueryable(),
      p => p.id,
      o => o.entity,
      (p, o) => new { p.id, p.name, other = o.First() }
    )
    .OrderByDescending(p => p.other.name);

Запрос отправлен на сервер:

[ 
  { "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
  { "$lookup" : {
    "from" : "others",
    "localField" : "_id",
    "foreignField" : "entity",
    "as" : "o"
  } },
  { "$project" : {
    "id" : "$_id",
    "name" : "$name",
    "other" : { "$arrayElemAt" : [ "$o", 0 ] },
    "_id" : 0
  } },
  { "$sort" : { "other.name" : -1 } }
]

Это почти идентично, но только с использованием другого интерфейса и создает немного другой оператор BSON, и на самом деле только из-за упрощенного именования в функциональных операторах. Это поднимает другую возможность простого использования $unwind полученного из SelectMany():

var query = entities.AsQueryable()
  .Where(p => listNames.Contains(p.name))
  .GroupJoin(
    others.AsQueryable(),
    p => p.id,
    o => o.entity,
    (p, o) => new { p.id, p.name, other = o }
  )
  .SelectMany(p => p.other, (p, other) => new { p.id, p.name, other })
  .OrderByDescending(p => p.other.name);

Запрос отправлен на сервер:

[
  { "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
  { "$lookup" : {
    "from" : "others",
    "localField" : "_id",
    "foreignField" : "entity",
    "as" : "o"
  }},
  { "$project" : {
    "id" : "$_id",
    "name" : "$name",
    "other" : "$o",
    "_id" : 0
  } },
  { "$unwind" : "$other" },
  { "$project" : {
    "id" : "$id",
    "name" : "$name",
    "other" : "$other",
    "_id" : 0
  }},
  { "$sort" : { "other.name" : -1 } }
]

Обычно размещение $unwind непосредственно после $lookup на самом деле является "оптимизированным шаблоном" для структуры агрегации. Тем не менее, драйвер .NET все портит в этой комбинации, заставляя $project между ними, а не используя подразумеваемое именование "as". Если бы не это, это на самом деле лучше, чем $arrayElemAt когда вы знаете, что у вас есть "один" связанный результат. Если вы хотите, чтобы $unwind "coalescence", то вам лучше использовать свободный интерфейс или другую форму, как будет показано позже.

Querable Природный

var query = from p in entities.AsQueryable()
            where listNames.Contains(p.name) 
            join o in others.AsQueryable() on p.id equals o.entity into joined
            select new { p.id, p.name, other = joined.First() }
            into p
            orderby p.other.name descending
            select p;

Запрос отправлен на сервер:

[
  { "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
  { "$lookup" : {
    "from" : "others",
    "localField" : "_id",
    "foreignField" : "entity",
    "as" : "joined"
  } },
  { "$project" : {
    "id" : "$_id",
    "name" : "$name",
    "other" : { "$arrayElemAt" : [ "$joined", 0 ] },
    "_id" : 0
  } },
  { "$sort" : { "other.name" : -1 } }
]

Все довольно знакомо и на самом деле просто функциональное наименование. Так же, как при использовании опции $unwind:

var query = from p in entities.AsQueryable()
            where listNames.Contains(p.name) 
            join o in others.AsQueryable() on p.id equals o.entity into joined
            from sub_o in joined.DefaultIfEmpty()
            select new { p.id, p.name, other = sub_o }
            into p
            orderby p.other.name descending
            select p;

Запрос отправлен на сервер:

[ 
  { "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
  { "$lookup" : {
    "from" : "others",
    "localField" : "_id",
    "foreignField" : "entity",
    "as" : "joined"
  } },
  { "$unwind" : { 
    "path" : "$joined", "preserveNullAndEmptyArrays" : true
  } }, 
  { "$project" : { 
    "id" : "$_id",
    "name" : "$name",
    "other" : "$joined",
    "_id" : 0
  } }, 
  { "$sort" : { "other.name" : -1 } }
]

Который на самом деле использует форму "оптимизированной коалесценции". Переводчик по-прежнему настаивает на добавлении $project так как нам нужен промежуточный select, чтобы сделать утверждение действительным.

Резюме

Таким образом, существует довольно много способов достичь того, что в основном является одним и тем же оператором запроса, с точно такими же результатами. Хотя вы "можете" BsonDocument форму JSON в BsonDocument и BsonDocument в BsonDocument команду Aggregate(), обычно лучше использовать естественные компоновщики или интерфейсы LINQ, поскольку они легко отображаются на один и тот же оператор.

Варианты с $unwind в основном показаны, потому что даже при "единственном" совпадении эта форма "коалесценции" на самом деле гораздо более оптимальна, чем использование $arrayElemAt для получения "первого" элемента массива. Это даже становится более важным с учетом таких вещей, как предел BSON, когда целевой массив $lookup может привести к тому, что родительский документ превысит 16 МБ без дальнейшей фильтрации. Здесь есть еще одна публикация о Aggregate $ lookup. Общий размер документов в соответствующем конвейере превышает максимальный размер документа, где я на самом деле обсуждаю, как избежать этого ограничения, используя такие параметры или другой синтаксис Lookup() доступный только для текущего интерфейса.,

Ответ 2

вот как это сделать с MongoDB.Entities. в тех случаях, когда два объекта находятся в отношении "один ко многим" или "многие ко многим", вы можете получить обратный доступ к отношениям без необходимости выполнять соединения вручную, как показано ниже. [Отказ от ответственности: я автор библиотеки]

using System;
using System.Linq;
using MongoDB.Entities;
using MongoDB.Driver.Linq;

namespace StackOverflow
{
    public class Program
    {
        public class Author : Entity
        {
            public string Name { get; set; }
            public Many<Book> Books { get; set; }

            public Author() => this.InitOneToMany(() => Books);
        }

        public class Book : Entity
        {
            public string Title { get; set; }
        }

        static void Main(string[] args)
        {
            new DB("test");

            var book = new Book { Title = "The Power Of Now" };
            book.Save();

            var author = new Author { Name = "Eckhart Tolle" };
            author.Save();

            author.Books.Add(book);

            //build a query for finding all books that has Power in the title.
            var bookQuery = DB.Queryable<Book>()
                              .Where(b => b.Title.Contains("Power"));

            //find all the authors of books that has a title with Power in them
            var authors = author.Books
                                .ParentsQueryable<Author>(bookQuery); //also can pass in an ID or array of IDs

            //get the result
            var result = authors.ToArray();

            //output the aggregation pipeline
            Console.WriteLine(authors.ToString());


            Console.ReadKey();
        }
    }
}