#c# #entity-framework #rest #asp.net-core #dto
#c# #entity-framework #rest #asp.net-core #dto
Вопрос:
У меня возникла проблема с принятием решения о том, как обрабатывать DTO для Entity Framework в следующем сценарии.
У меня есть таблица клиентов, и в отдельной таблице у меня есть контакты клиента. Моя проблема связана с тем фактом, что клиент может быть изначально создан без каких-либо контактов. Таким образом, обновление (HTTP PUT) может обновить существующего клиента, добавив новый контакт. Или это может привести к обновлению существующего клиента путем обновления существующего контакта. В последнем случае моему объекту contact в DTO потребуется идентификатор. В первом случае этого не произойдет.
public class UpdateCustomer {
int Id {get; set;}
public string Name {get; set;}
public ICollection<Contact> Contacts {get; set;}
}
Контактный DTO может выглядеть следующим образом:
public class Contact {
<Will need contactID on update>
public CustomerId {get; set; }
public string ContactType {get; set;}
public string PhoneNumber {get; set;}
}
Это отлично работает, когда пользователь хочет создать клиента и создать контакты одновременно, используя post. Объектам контактов идентификаторы не нужны. Но, допустим, они создают клиента с одним контактом. Теперь они хотят обновить этого клиента, обновить существующий контакт, но также добавить новый контакт. Это все равно будет вызов метода Put, но одному контакту в списке потребуется идентификатор, другому — нет.
Я вижу пару способов решить эту проблему. Наиболее очевидным является разделение создания / обновлений клиента и создания / обновлений контакта на отдельные запросы. Теперь у меня могут быть отдельные объекты для каждого, один с идентификатором, другой без. Это то, к чему я склоняюсь, и клиентскому приложению просто нужно было бы выполнить два отдельных вызова API.
Другой вариант — независимо включить идентификатор в объект Contact на стороне сервера. Если клиентское приложение не предоставляет идентификатор контакта, он будет равен null. Если я вижу, что оно равно null, я могу попытаться добавить, если оно не равно null, я могу попытаться обновить.
Мне нравится первая идея, потому что она сохраняет thinks раздельными, но приводит к дополнительным вызовам API. Мне нравится второй, потому что он позволяет одним вызовом API обновлять все компоненты, связанные с клиентом, одновременно. Недостатком является то, что я могу предположить, что конечный пользователь хочет добавить, когда они действительно хотят обновления, если они оставляют идентификатор.
Надеюсь, это имеет смысл. Я просто ищу предложения или обратную связь.
Обновление: Используя предложение Roar, ниже приведено мое рабочее решение. Обратите внимание, что в моем исходном сообщении я ссылался на класс как customer, но на самом деле это Client в моей базе данных. Я только что обнаружил, что использование Client в сообщении о веб-api сбивает с толку.
public async Task UpdateClient(Dtos.Shared.Client client)
{
if (!ClientExists(client.ClientId))
{
throw new ClientNotFoundException();
}
Client clientToUpdate = await AdministrativeContext.ListClients()
.Where(c => c.ClientUuid == client.ClientId)
.FirstOrDefaultAsync();
clientToUpdate.ClientName = client.ClientName;
//filter existing db elements that are still in DTO as well
clientToUpdate.Contacts = clientToUpdate.Contacts
.Where(c => client.Contacts.Any(con => con.ContactId == c.ContactUuid))
.ToList();
client.Contacts.ToList().ForEach(async contact =>
{
if (contact.ContactId == null || contact.ContactId == Guid.Empty) //if guid missing, we want to add
{
clientToUpdate.Contacts.Add(new Entities.Contact()
{
ClientId = await AdministrativeContext.GetClientKeyFromGuidAsync(client.ClientId),
ContactUuid = Guid.NewGuid(),
FirstName = contact.FirstName,
LastName = contact.LastName,
Address = contact.Address,
Email = contact.Email,
Phone = contact.Phone,
TypeId = (await AdministrativeContext.ContactTypes.Where(ct => ct.ContactTypeName == contact.ContactType).FirstOrDefaultAsync()).ContactTypeId
});
}
else //guid defined means update
{
Contact existingContact = clientToUpdate.Contacts.Where(c => contact.ContactId == c.ContactUuid).FirstOrDefault();
existingContact.ClientId = await AdministrativeContext.GetClientKeyFromGuidAsync(client.ClientId);
existingContact.Address = contact.Address;
existingContact.Email = contact.Email;
existingContact.FirstName = contact.FirstName;
existingContact.LastName = contact.LastName;
existingContact.Phone = contact.Phone;
existingContact.TypeId = (await AdministrativeContext.ContactTypes
.Where(ct => ct.ContactTypeName == contact.ContactType)
.FirstOrDefaultAsync()).ContactTypeId;
}
});
await AdministrativeContext.SaveChangesAsync();
}
Комментарии:
1. Похоже, вы выполнили необходимый анализ. Два перечисленных вами варианта являются жизнеспособными. Просто вопрос в том, какой из них будет проще в долгосрочной перспективе для продолжения обслуживания вашего приложения. Если ваши пользователи будут часто обновлять / добавлять контакты, то первый вариант, по-видимому, имеет больше смысла. Если это всего лишь одноразовый тип, то второй вариант может быть лучше.
Ответ №1:
Обычно я решаю проблему обновления следующим образом в одном запросе.
- Получение клиента из базы данных с заполненной коллекцией контактов
- Выполните итерацию по всем контактам в экземпляре базы данных клиента. Если нет в DTO клиента, то удалите contact из коллекции контактов в экземпляре базы данных клиента
- Выполните итерацию по всем контактам из DTO. Если идентификатор контакта # = 0 (или, возможно, null в данном случае), добавьте новый контакт в коллекцию контактов в экземпляре базы данных клиента, иначе обновите существующий экземпляр контакта в коллекции контактов
- Обновите экземпляр базы данных клиента оставшимися данными из DTO
- Сохранение клиента вместе с коллекцией контактов в БД (требуется каскадирование)
Обычно я работаю с NHibernate, но этот принцип должен работать и с EF.
Плюсы:
- Простой, обрабатывает создание, обновление и удаление для контактов вместе с обновлением клиента
- Единый вызов db для обновления
Минусы:
- Требует, чтобы все контакты были в полезной нагрузке при каждом обновлении
Комментарии:
1. Мне очень нравится этот ответ, и вот почему. У меня есть уникальное ограничение на мою таблицу контактов. Это ограничение относится к типу клиента и контакта. У меня есть тип контакта для выставления счетов и тип основного контакта. Проблема в том, что если у клиента уже есть по одному из них, и вы хотите изменить выставление счетов на первичное, а первичное — на выставление счетов, вы не можете. Изменение по одному вызову API для каждого из них каждый раз приведет к уникальному нарушению ограничений, и я не смогу обновиться. Передавая весь список, как вы делаете, я полагаю, этого можно избежать.
2. Хорошие новости. Давайте поддерживать связь, если возникнут проблемы.