Как вы размещаете отношение «многие ко многим ко многим» в REST API?

#c# #api #rest #many-to-many #relational

#c# #API #rest #многие ко многим #реляционное

Вопрос:

Использование EF Core

Мы пытаемся получить всю информацию об оценке, которая включает ее группы и всех назначенных пользователей. Смотрите схему базы данных

Что работает в следующем порядке;

  1. HttpPost (api / Assessment / aID / groups) пустой группы для оценки
  2. HttpPost (api / Group / gID / users) пользователей в существующую группу

Чего мы пытаемся достичь (ссылочный код — это другой пример, но тот же принцип);

  • HttpPost (api / Assessment / aID / groups), где группа уже содержит список пользователей. При попытке выполнить это, a possible object cycle was detected which is not supported.
 This piece of code is currently throwing a NullReference on Address
-------------------------------------------------------------------

   Group groupToCreate = new Group { Name = dto.Name, Description = dto.Description };

   foreach (var u in dto.Users)
   {
       groupToCreate.AddUser(new User
        {
             Name = u.Name,
             Email = u.Email,
             Address = new Address
             {
                 Country = u.Address.Country,
                 City = u.Address.City,
                 PostalCode = u.Address.PostalCode,
                 Street = u.Address.Street,
                 HouseNr = u.Address.HouseNr,
                 BusNr = u.Address.BusNr
             }
        });
    }

   _groupRepository.Add(groupToCreate);
   _groupRepository.SaveChanges();

   return groupToCreate;
  
  • HttpGet (api / Assessment), который отображает назначенные ему группы и связанных пользователей.
 This seems to be working
------------------------
   groupList = _groups.Select(g => new GroupDTO
   {
       Name = g.Name,
       Description = g.Description,
       Users = g.GroupUsers.Select(u => new UserDTO
       {
           Name = u.User.Name,
           Email = u.User.Email,
           Address = new AddressDTO
           {
              Country = u.User.Address.Country,
              City = u.User.Address.City,
              PostalCode = u.User.Address.PostalCode,
              Street = u.User.Address.Street,
              HouseNr = u.User.Address.HouseNr,
              BusNr = u.User.Address.BusNr
           }
       }).ToList()
   }).ToList();
  

Ссылки:
Оценка группы пользователей


AssessmentRepo

Комментарии:

1. В дополнение к моим комментариям ниже, прикрепление изображений кода гораздо менее полезно, чем просто добавление фрагментов кода к сообщению; SO предназначен для того, чтобы легко задавать вопросы, связанные с кодом, поэтому вам и всем остальным на самом деле проще копировать / вставлять соответствующие фрагменты кода, чем этодля размещения изображений в формате png…

Ответ №1:

Трудно сказать, учитывая детали, которые вы предоставляете, но я предполагаю, что это связано с наличием свойств двусторонней навигации? Вы используете EF здесь?

Например, если у вашего пользователя есть свойство навигации, позволяющее получить доступ к свойству пользователя Group , но Group у a есть коллекция User объектов, тогда каждый из этих пользователей сам будет иметь Group выраженное внутри них … тогда при попытке выразить это он может легко застрять в цикле, например, пользователь будет выглядеть как:

 {
  "Name":"user name",
  "Group":{
     "Name":"group1",
     "Users":[
       {
          "Name":"user name",
          "Group":{
            "Name":"group1",
            "Users":{
              ....
            }
          }
       }
     ]
  }
}
  

.. потому что a User имеет a Group , и Group у него есть список User объектов, и у каждого из них есть Group … и т.д.

Это своего рода проблема, возникающая из-за смешивания вашего уровня данных и объектов DTO. Измените свою систему так, чтобы объекты, возвращаемые вашими методами REST, были новыми объектами, разработанными в соответствии с требованиями API / интерфейса. Эти объекты могут выглядеть очень похожими на ваши модели БД (по крайней мере, изначально), но они не должны быть одинаковыми объектами.

Создавайте совершенно новые объекты, которые не имеют никаких логических или навигационных свойств и существуют только для передачи информации обратно потребителям API. Например, простой класс для предоставления списка групп пользователей и пользователей в этих группах может быть определен как:

 public class UserDto
{
   public string UserName { get; set; }
   public IEnumerable<string> Groups { get; set; }
}

public class UserListDto
{
   public IEnumerable<UserDto> Users { get; set; }
}
  

И тогда ваше действие контроллера может выполнять что-то вроде:

 var users = userService.GetAllUsers();

var result = new UserListDto { 
    Users = users.Select(u => new UserDto{
      UserName = u.Name,
      Groups = u.Groups.Select(g => g.Name)
      }
   };

return Ok(result);
  

.. Таким образом, сериализуемая для ответа вещь не имеет каких-либо сложных отношений для согласования, и, что более важно, изменение того, как вы внутренне храните и работаете с данными, не повлияет на внешний контракт вашего API — потребители API могут продолжать видеть точно такую же информацию, но как вы храните и компилируетеэто может резко измениться.

Заманчиво думать, что «Данные, которые мне нужно вернуть, в основном такие же, как и то, как я храню их внутри, поэтому просто повторно используйте эти классы», но это не очень хорошая идея и только в долгосрочной перспективе создаст проблемы.

Чтобы избежать необходимости (повторного) написания большого количества кода для «преобразования» одного объекта в другой, я бы рекомендовал изучить что-то вроде AutoMapper, поскольку это может сделать это довольно легко повторно используемым и позволить вам поместить все эти «переводные» вещи в одно место.

Комментарии:

1. Мы действительно используем ядро EF, проблема, о которой вы упомянули, — это то, что мы уже решили, поскольку это действительно может вызвать цикл объекта. Что происходит, так это то, что при запросе get или post на два уровня глубже (оценка — пользователь) это вызывает цикл объекта, но при последующем добавлении пустой группы в оценку и пользователей в группу, похоже, работает правильно. Спасибо за помощь, какие классы помогли бы предоставить информацию?

2. я думаю, что ваша проблема связана с сериализацией JSON. Объекты, которые вы пытаетесь вернуть из своих вызовов API, не могут быть сериализованы, потому что они похожи по структуре на модели EF и имеют циклические ссылки (не совсем видно, где они находятся в ваших прикрепленных изображениях .png; GroupUser и GroupAssessment классы не показаны). Что вам нужно сделать, это точно определить, что ваш потребитель API хочет от API, и создать класс, который предоставляет только эту информацию. Я отредактировал ответ, чтобы немного расширить его.

3. Я ценю ваш ответ, это очень помогло! Как бы вы создали свой HttpPost? Я предполагаю, что вы будете передавать объект DTO для данных и присваивать эти данные обычным объектам, которые связаны вместе. Тем не менее, при попытке сделать это, я, кажется, получаю NullReference при создании обычных объектов.

4. Я предполагаю, что по крайней мере один из User объектов в dto.Users коллекции имеет нулевой адрес. это может быть связано с тем, что оно не включено во входной объект, или что-то простое, например, опечатка (например, входной JSON имеет ‘Adress’, а не ‘Address’), но вы должны просто проверить, что адрес не равен null, прежде чем пытаться его использовать. Здесь есть много сообщений о защите от исключений с нулевыми ссылками.