Проблемы MongoDB с переводом JS-запроса на C#

У меня есть коллекция, в которой хранится информация о сиренах – уведомлениях. Мне нужно получить сводную статистику пользователей по (длинному) идентификатору пользователя:

  1. Количество принадлежащих сирен;
  2. Количество подписок;
  3. Количество сирен, за которые несет ответственность пользователь.

Структура документа Siren (обязательные поля только для вопросов):

{
  "_id": ObjectId( "65fc94593273fc2ab6ff8960"),
  "ownerid": NumberLong( "99999999"), 
  "listener": [
    NumberLong( "11111111"),
    NumberLong( "00000000")
  ],
  "responsible": [
    NumberLong( "11111111")
    ]
}

После двух дней борьбы я сделал рабочий запрос через JS.

db.sirens.aggregate([
  {
    "$match": {
      "$or": [
        { "ownerid": userId },
        { "listener": userId },
        { "responsible": userId }
      ]}},
  {
    "$group": {
      "_id": null,
      "owner": { "$sum": { "$cond": [{ "$eq": ["$ownerid", userId ] }, 1, 0] }},
      "responsible": { "$sum": { "$cond": [{ "$and": [
                 { "$ne": ["$responsible", null] },
                 { "$isArray": "$responsible" }, 
                 { "$in": [userId, "$responsible"] }
              ] }, 1,0 ] }}
      ,"listener": { "$sum": { "$cond": [{ "$and": [
                 { "$ne": ["$listener", null] },
                 { "$isArray": "$listener" }, 
                 { "$in": [userId, "$listener"] }
              ] }, 1,0 ] }}   
    }
  }
]);

Но вот проблема, которую я не могу решить. поля «слушатель» и «ответственный» могут отсутствовать. А в JS это условие:{ "$isArray": "$responsible" }, обрабатывает ситуацию. Но я не могу сделать то же самое на C#.

Вот код, который у меня есть:

var query = sirens.AsQueryable<SirenRepresentation>()
    .Where(_sirena => _sirena.OwnerId == userId
                      || (_sirena.Listener != null && _sirena.Listener.Any(x => x == userId))
                      || (_sirena.Responsible != null && _sirena.Responsible.Any(x => x == userId)))
    .GroupBy(s => true)
    .Select(g => new UserStatistics
    {
      SirenasCount = g.Sum(_siren => _siren.OwnerId == userId ? 1 : 0),
      Subscriptions = g.Sum(_siren => (_siren.Listener != null && _siren.Listener.Contains(userId)) ? 1 : 0),
      Responsible = g.Sum(_siren => (_siren.Responsible != null && _siren.Responsible.Contains(userId)) ? 1 : 0)
    });
    
public class SirenRepresentation
{
  [BsonId]
  public ObjectId Id { get; set; }
  [BsonElement("ownerid"), BsonRepresentation(BsonType.Int64)]
  public long OwnerId { get; set; }
  [BsonRepresentation(BsonType.Int64)]
  [BsonElement("listener")]
  public long[] Listener { get;  set; } = [];
  [BsonRepresentation(BsonType.Int64)]
  [BsonElement("responsible")]
  public long[] Responsible { get;  set; } = [];
  [BsonElement("requests")]
//...
}

public class UserStatistics{
  public int SirenasCount{get;set;}
  public int Subscriptions{get;set;}
  public int Responsible{get;set;}
}

Когда одно из полей отсутствует, перехватывается исключение:

Произошло исключение: CLR/MongoDB.Driver.MongoCommandException. исключение типа «MongoDB.Driver.MongoCommandException» произошло в System.Private.CoreLib.dll, но не обрабатывался в пользовательском коде: 'Команда Ошибка агрегирования: ошибка PlanExecutor во время агрегации:: вызвана :: $in требует массив в качестве второго аргумента, найдено: отсутствует.'


Обновлять. Фиксированный метод:

public async Task<UserStatistics> Get(long userId)
{
  var query = sirens.AsQueryable()
  .Where(_sirena => _sirena.OwnerId == userId
                    || _sirena.Listener.Any(x => x == userId)
                    || _sirena.Responsible.Any(x => x == userId))
  .GroupBy(keySelector: x => true,
    resultSelector: (_, _sirens) => new UserStatistics
      {
        SirenasCount = _sirens.Sum(x => x.OwnerId == userId ? 1 : 0),
        Subscriptions = _sirens.Sum(_sirena => (_sirena.Listener ?? new long[] { }).Contains(userId) ? 1 : 0),
        Responsible = _sirens.Sum(_sirena => (_sirena.Responsible ?? new long[] { }).Contains(userId) ? 1 : 0)
      })
  .FirstOrDefaultAsync();
  return await query;
}

Большое спасибо Ён Шуну <3

P.S. MongoDB.Driver 2.24 также обеспечивает поведение для перевода some_array is Array, но, к сожалению, в нем есть ошибки. Это переводится is Array как выражение:

"$cond": {
    "if": {
        "$and": [{
                "$or": [{
                        "$eq": ["$some_array._t", "Array"]
                    }, {
                        "$and": [{
                                "$isArray": "$some_array._t"
                            }, {
                                "$in": ["Array", "$some_array._t"]
                            }
                        ...

А в моем случае для MongoDB v.7.0.6 это работает некорректно. Но если мы изменим "$eq": ["$some_array._t", "Array"] -> {"$isArray": "$some_array"}, то всё заработает.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
64
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Поскольку ваши поля Listener и Responsible, возможно, отсутствуют, вы можете указать для поля значение по умолчанию с помощью:

(_siren.Listener ?? new long[] { })

Таким образом, при преобразовании в запрос MongoDB это эквивалентно:

{ "$ifNull" : ["$responsible", []] }

Полный запрос:

var query = sirens.AsQueryable<SirenRepresentation>()
    .Where(_siren => _siren.OwnerId == userId
        || (_siren.Listener != null 
            && (_siren.Listener ?? new long[] { }).Any(x => x == userId))
        || (_siren.Responsible != null 
            && (_siren.Responsible ?? new long[] { }).Any(x => x == userId)))
    .GroupBy(s => true)
    .Select(g => new UserStatistics
    {
        SirenasCount = g.Sum(_siren => _siren.OwnerId == userId ? 1 : 0),
        Subscriptions = g.Sum(_siren => (_siren.Listener != null 
            && (_siren.Listener ?? new long[] { }).Contains(userId)) ? 1 : 0),
        Responsible = g.Sum(_siren => (_siren.Responsible != null 
            && (_siren.Responsible ?? new long[] { }).Contains(userId)) ? 1 : 0)
    });

Это не работает таким образом. Потому что это не LINQ Select, а MongoDb.Driver.Linq Select. Этот метод преобразует условия из синтаксиса C# в инструкции json «под капотом» MongoDB. Так что это не может быть переведено (_siren.Listener ?? new long[] { }). Там есть перевод _siren.Listener is Array, но, к сожалению, есть ошибка.

HedgehogNSK 07.04.2024 00:07

Какую версию драйвера MongoDB .NET вы используете? Если вы используете LINQ Provider v3 и сможете достичь: MongoClientSettings settings = MongoClientSettings.FromConnectionString(mongoUrl); settings.LinqProvider = MongoDB.Driver.Linq.LinqProvider.V3;

Yong Shun 07.04.2024 04:09

Я использую: MongoDB.Driver - 2.24.0. А MongoClient.Client.Settings.LinqProvider* по умолчанию имеет значение V3.

HedgehogNSK 08.04.2024 05:17

В противном случае я бы предложил вам передать весь запрос как Экспортировать запрос на определенный язык без необходимости преобразования в запрос LINQ.

Yong Shun 08.04.2024 05:38

Да, я тестировал версию 2.24.0 и смог получить результат без ошибок. Было бы здорово, если бы вы рассказали, какую ошибку вы получили. Спасибо.

Yong Shun 08.04.2024 05:58

Да. Ты был прав. Когда я посмотрел на результат, я неправильно его интерпретировал. Спасибо. Оно работает. Тем не менее, в «is Array» есть ошибка. Полагаю, мне нужно опубликовать отчет об ошибке в MongoDB JIRA.

HedgehogNSK 08.04.2024 08:35

Я отправил отчет об ошибке команде MongoDB. И член их команды очень помог мне разобраться в преобразовании конструкций языка C# в строку запроса json. Особая благодарность Борису Догадову.

Короче говоря:

  1. Проверка на нулевое значение — это не то же самое, что проверка на отсутствие поля.
  2. Мы можем использовать is T для целей десериализации некоторого документа в некоторый объект типа T. В этом случае поле документа _t должно быть установлено. Это описано в этой статье. Но это неподходящий способ проверить, является ли поле массивом.
  3. MongoDB.Driver.Mql — статический класс, предоставляющий инструменты для проверки отсутствующих полей.

В результате мы можем проверить недостающее поле следующим образом:

Responsible = _sirens.Sum(_sirena => (Mql.Exists(_sirena.Responsible) 
                   && _sirena.Responsible.Contains(userId)) ? 1 : 0)

или

sirens.AsQueryable().Where(x=> Mql.Exists(x.Responsible));

Помните, что методы Mql применимы только для запросов LINQ. Также есть подробный ответ другому пользователю о тонкостях обработки LINQ

Другие вопросы по теме