EF 6.1.1 и Web API Ошибка при добавлении сущности с дочерними элементами
Когда я пытаюсь добавить сущность с новыми детьми, я получаю исключение InvalidOperationException в EntityFramework.dll.
Я установил небольшое тестовое приложение, чтобы попытаться понять эту проблему.
У меня есть две модели: родитель и ребенок.
public class Parent
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid ParentId { get; set; }
public String Name { get; set; }
public List<Child> Children { get; set; }
}
public class Child
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid ChildId { get; set; }
public Guid ParentId { get; set; }
public string Name { get; set; }
// Navigation
[ForeignKey("ParentId")]
public Parent Parent { get; set; }
}
на стороне WebAPI у меня есть контроллер ParentController
// PUT: api/Parents/5
[ResponseType(typeof(void))]
public async Task<IHttpActionResult> PutParent(Guid id, Parent parent)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != parent.ParentId)
{
return BadRequest();
}
db.Entry(parent).State = EntityState.Modified;
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ParentExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return StatusCode(HttpStatusCode.NoContent);
}
Я собрал приложение WPF для работы с API.
Нажатием кнопки:
private async void Button_Click_1(object sender, RoutedEventArgs e)
{
ParentApi parentApi = new ParentApi();
var response = await parentApi.GetParents();
if(response.ResponseCode.Equals(200))
{
var parent = ((List<Parent>)response.ResponseObject).Where(prnt => prnt.Name.Equals("Parent1", StringComparison.Ordinal)).Single();
if(parent != null)
{
// Put child entity/
if (parent.Children == null)
parent.Children = new List<Child>();
Child newChild = new Child();
newChild.Name = "Child One";
parent.Children.Add(newChild);
response = await parentApi.PutParent(parent.ParentId, parent);
if(response.ResponseCode.Equals(200))
{
// Success
Debug.WriteLine(response.ResponseObject.ToString());
}
else
{
// Other/
if (response.ResponseObject != null)
Debug.WriteLine(response.ResponseObject.ToString());
}
}
}
}
ParentAPi выглядит так:
public class ParentApi : ApiBase
{
public async Task<ApiConsumerResponse> GetParents()
{
return await GetAsync<Parent>("http://localhost:1380/api/Parents/");
}
public async Task<ApiConsumerResponse> PutParent(Guid parentId, Parent parent)
{
return await PutAsync<Parent>(parent, "http://localhost:1380/api/Parents/" + parentId);
}
}
ApiBase и ApiConsumerResponse выглядят так:
public class ApiBase
{
readonly RequestFactory _requester = new RequestFactory();
public async Task<ApiConsumerResponse> GetAsync<T>(string uri)
{
ApiConsumerResponse result = new ApiConsumerResponse();
try
{
var response = await _requester.Get(new Uri(uri));
result.ResponseCode = response.ResponseCode;
result.ReasonPhrase = response.ReasonPhrase;
if (result.ResponseCode == 200)
{
result.ResponseObject = await Task.Factory.StartNew(
() => JsonConvert.DeserializeObject<List<T>>(
response.BodyContentJsonString));
}
else
{
string msg = response.ReasonPhrase + " - " + response.BodyContentJsonString;
result.ErrorReceived = true;
}
}
catch (Newtonsoft.Json.JsonReaderException jsonE)
{
result.ErrorReceived = true;
}
catch (Exception e)
{
// Some other error occurred.
result.ErrorReceived = true;
}
return result;
}
public async Task<ApiConsumerResponse> PutAsync<T>(T apiModel, string uri)
{
ApiConsumerResponse result = new ApiConsumerResponse();
try
{
string json = await Task.Factory.StartNew(
() => JsonConvert.SerializeObject(
apiModel, Formatting.Indented));
var response = await _requester.Put(new Uri(uri), json);
result.ResponseCode = response.ResponseCode;
result.ReasonPhrase = response.ReasonPhrase;
// if 200: OK
if (response.ResponseCode.Equals(200))
{
result.ResponseObject = await Task.Factory.StartNew(
() => JsonConvert.DeserializeObject<T>(
response.BodyContentJsonString));
}
else
{
string msg = response.ReasonPhrase + " - " + response.BodyContentJsonString;
result.ErrorReceived = true;
}
}
catch (Newtonsoft.Json.JsonReaderException jsonE)
{
result.ErrorReceived = true;
}
catch (Exception e)
{
// Some other error occurred.
result.ErrorReceived = true;}
return result;
}
}
public class ApiConsumerResponse
{
public int ResponseCode { get; set; }
public string ReasonPhrase { get; set; }
public object ResponseObject { get; set; }
public bool ErrorReceived { get; set; }
}
RequestFactory (который не является фабрикой) и его класс ответа выглядят так:
public class RequestFactory
{
public async Task<NetworkWebRequestMakerResponse> Get(Uri uri)
{
if (uri.UserEscaped)
{
uri = new Uri(Uri.EscapeUriString(uri.OriginalString));
}
using (var client = new HttpClient())
{
try
{
client.Timeout = TimeSpan.FromSeconds(60);
var response = await client.GetAsync(uri);
var stringResponse = await response.Content.ReadAsStringAsync();
return new NetworkWebRequestMakerResponse()
{
UnknownErrorReceived = false,
UnknonErrorExceptionObject = null,
ResponseCode = (int)response.StatusCode,
ReasonPhrase = response.ReasonPhrase,
BodyContentJsonString = stringResponse,
};
}
catch (Exception Ex)
{
return new NetworkWebRequestMakerResponse()
{
UnknownErrorReceived = true,
UnknonErrorExceptionObject = Ex,
ResponseCode = -1,
ReasonPhrase = "NONE",
BodyContentJsonString = "{NONE}",
};
}
}
}
public async Task<NetworkWebRequestMakerResponse> Post(Uri url, string json)
{
using (var client = new HttpClient())
{
HttpResponseMessage response;
try
{
Debug.WriteLine("POSTING JSON: " + json);
var content = new StringContent(json);
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(@"application/json");
response = await client.PostAsync(url, content);
var stringResponse = await response.Content.ReadAsStringAsync();
/*
* For the reason given in the post below I will not call EnsureSuccessCode as it blows away the data
* http://stackru.com/questions/14208188/how-to-get-the-json-error-message-from-httprequestexception
*
*/
// response.EnsureSuccessStatusCode();
return new NetworkWebRequestMakerResponse()
{
UnknownErrorReceived = false,
UnknonErrorExceptionObject = null,
ResponseCode = (int)response.StatusCode,
ReasonPhrase = response.ReasonPhrase,
BodyContentJsonString = stringResponse,
};
}
catch (Exception Ex)
{
return new NetworkWebRequestMakerResponse()
{
UnknownErrorReceived = true,
UnknonErrorExceptionObject = Ex,
ResponseCode = -1,
ReasonPhrase = "NONE",
BodyContentJsonString = "{NONE}",
};
}
}
}
public async Task<NetworkWebRequestMakerResponse> Put(Uri url, string json)
{
using (var client = new HttpClient())
{
HttpResponseMessage response;
try
{
Debug.WriteLine("PUTING JSON: " + json);
var content = new StringContent(json);
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(@"application/json");
response = await client.PutAsync(url, content);
var stringResponse = await response.Content.ReadAsStringAsync();
/*
* For the reason given in the post below I will not call EnsureSuccessCode as it blows away the data
* http://stackru.com/questions/14208188/how-to-get-the-json-error-message-from-httprequestexception
*
*/
// response.EnsureSuccessStatusCode();
return new NetworkWebRequestMakerResponse()
{
UnknownErrorReceived = false,
UnknonErrorExceptionObject = null,
ResponseCode = (int)response.StatusCode,
ReasonPhrase = response.ReasonPhrase,
BodyContentJsonString = stringResponse,
};
}
catch (Exception Ex)
{
return new NetworkWebRequestMakerResponse()
{
UnknownErrorReceived = true,
UnknonErrorExceptionObject = Ex,
ResponseCode = -1,
ReasonPhrase = "NONE",
BodyContentJsonString = "{NONE}",
};
}
}
}
public async Task<NetworkWebRequestMakerResponse> Delete(Uri url)
{
using (var client = new HttpClient())
{
HttpResponseMessage response;
try
{
response = await client.DeleteAsync(url);
var stringResponse = await response.Content.ReadAsStringAsync();
/*
* For the reason given in the post below I will not call EnsureSuccessCode as it blows away the data
* http://stackru.com/questions/14208188/how-to-get-the-json-error-message-from-httprequestexception
*
*/
// response.EnsureSuccessStatusCode();
return new NetworkWebRequestMakerResponse()
{
UnknownErrorReceived = false,
UnknonErrorExceptionObject = null,
ResponseCode = (int)response.StatusCode,
ReasonPhrase = response.ReasonPhrase,
BodyContentJsonString = stringResponse,
};
}
catch (Exception Ex)
{
return new NetworkWebRequestMakerResponse()
{
UnknownErrorReceived = true,
UnknonErrorExceptionObject = Ex,
ResponseCode = -1,
ReasonPhrase = "NONE",
BodyContentJsonString = "{NONE}",
};
}
}
}
}
public class NetworkWebRequestMakerResponse
{
public bool UnknownErrorReceived { get; set; }
public Exception UnknonErrorExceptionObject { get; set; }
public int ResponseCode { get; set; }
public string ReasonPhrase { get; set; }
public string BodyContentJsonString { get; set; }
}
Так что все хорошо. Тестируя метод Get (не показан), он возвращает родительские объекты - GOOD.
У меня проблема, когда я пытаюсь "поставить" родительскую сущность с новой дочерней сущностью. Как показано в методе Button_Click.
Однако родительский объект с новым дочерним элементом поступает в parentController, когда я пытаюсь установить состояние как измененное:
db.Entry(parent).State = EntityState.Modified;
Выдается ошибка: Нарушение ограничения ссылочной целостности: Значения свойства 'Parent.ParentId' на одном конце отношения не совпадают со значением (ями) свойства 'Child.ParentId' на другом конце.
Теперь в качестве теста я изменил метод PUT на контроллере, чтобы эмулировать попытку клиента.
Модифицированный метод PUT:
public async Task<IHttpActionResult> PutParent(Guid id, Parent parent)
{
parent = db.Parents.Where(pe => pe.Name.Equals("Parent1", StringComparison.Ordinal)).Single();
var child = new Child();
child.Name = "Billy";
if (parent.Children == null)
parent.Children = new List<Child>();
parent.Children.Add(child);
db.Entry(parent).State = EntityState.Modified;
var result = await db.SaveChangesAsync();
Debug.Write(result.ToString());
}
Который работает отлично. Дочерний объект добавляется в БД, обновляется ParentID и генерируется его собственный ключ.
Так почему же объект, который попадает через провод, взрывается EF?
Сначала я попытался прикрепить объект (db.Parents.Attach(parent);), но это выдает ту же ошибку.
Я не совсем понимаю.
1 ответ
Entity Framework необходимо отслеживать объекты, чтобы знать, куда и в каком направлении генерирует SQL-запрос, и частично это делается вами, устанавливая состояние объекта, так что если вы устанавливаете состояние родителя, которое будет изменено, но новое потомство состояние не было установлено как добавленное (по умолчанию не изменено), здесь структура объекта будет обрабатывать этот объект как уже существующий в памяти, а это не так.
но когда добавляется дочерний элемент в список дочерних элементов в API, инфраструктура сущностей устанавливает состояние дочернего элемента для добавления и генерирует SQL-код для вставки нового дочернего элемента и соответствующей ссылки на идентификаторы.
Надеюсь, это поможет.
РЕДАКТИРОВАТЬ В случае отключенного сценария, когда вы отправляете объекты по сети для изменения, добавления, удаления объектов, я определяю перечисление, которое будет передаваться с каждым Dto/ объектом, который я отправляю клиенту, и клиент будет изменять это свойство чтобы сервер знал, каково состояние каждого объекта, когда вы пытаетесь сохранить весь граф с помощью Entity Framework, поэтому перечисление будет выглядеть так:
public enum ObjectState
{
/// <summary>
/// Entity wasn't changed.
/// </summary>
Unchanged,
/// <summary>
/// Entity is new and needs to be added.
/// </summary>
Added,
/// <summary>
/// Entity has been modified.
/// </summary>
Modified,
/// <summary>
/// Entity has been deleted (physical delete).
/// </summary>
Deleted
}
а затем я определяю метод, который будет переводить это значение перечисления в состояние сущности, о котором знает структура сущности, у моего метода будет что-то вроде этого
// I do this before when the dbcontext about to be saved :
foreach (var dbEntityEntry in ChangeTracker.Entries())
{
var entityState = dbEntityEntry.Entity as IObjectState;
if (entityState == null)
throw new InvalidCastException(
"All entites must implement " +
"the IObjectState interface, this interface " +
"must be implemented so each entites state" +
"can explicitely determined when updating graphs.");
**dbEntityEntry.State = StateHelper.ConvertState(entityState.ObjectState);**
var trackableObject = dbEntityEntry.Entity as ITrackableObject;
// we need to set/update trackable properties
if (trackableObject == null)
{
continue;
}
var dateTime = DateTime.Now;
// set createddate only for added entities
if (entityState.ObjectState == ObjectState.Added)
{
trackableObject.CreatedDate = dateTime;
trackableObject.CreatedUserId = userId;
}
// set LastUpdatedDate for any case other than Unchanged
if (entityState.ObjectState != ObjectState.Unchanged)
{
trackableObject.LastUpdatedDate = dateTime;
trackableObject.LastUpdatedUserId = userId;
}
}
И, наконец, это мой вспомогательный класс для преобразования состояний из моего ObjectState => EF State и наоборот.
public class StateHelper
{
public static EntityState ConvertState(ObjectState state)
{
switch (state)
{
case ObjectState.Added:
return EntityState.Added;
case ObjectState.Modified:
return EntityState.Modified;
case ObjectState.Deleted:
return EntityState.Deleted;
default:
return EntityState.Unchanged;
}
}
public static ObjectState ConvertState(EntityState state)
{
switch (state)
{
case EntityState.Detached:
return ObjectState.Unchanged;
case EntityState.Unchanged:
return ObjectState.Unchanged;
case EntityState.Added:
return ObjectState.Added;
case EntityState.Deleted:
return ObjectState.Deleted;
case EntityState.Modified:
return ObjectState.Modified;
default:
throw new ArgumentOutOfRangeException("state");
}
}
}
надеюсь, это поможет.