当我尝试添加具有新子节点的实体时,我在EntityFramework.dll中得到InvalidOperationException。
我已经设置了一个小型测试应用,试图了解这个问题。
我有两个模型:父母和孩子。
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://stackoverflow.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://stackoverflow.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://stackoverflow.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。
我遇到的问题是当我尝试使用新的子实体'PUT'父实体时。如Button_Click方法中所示。
具有新子节点的Parent实体到达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());
}
完美无缺。将Child添加到数据库中,更新ParentID,并生成自己的密钥。
那么为什么遇到电线的物体会炸毁EF?
我尝试首先附加对象(db.Parents.Attach(parent);)但是会抛出相同的错误。
我很困惑。
答案 0 :(得分:1)
实体框架需要跟踪对象以了解哪些对象在哪里并相应地生成SQL查询,部分内容由您通过设置对象的状态来完成,因此如果您将父级的状态设置为要修改但是新子项的状态未设置为“添加”(默认为Unchanged),此处的实体frameowork会将此对象视为已存在于内存中,而实际情况并非如此。
但是当将子项添加到API中的子项列表时,实体框架会将Child的状态设置为Added,并生成SQL以插入新子项并相应地链接ID。
希望有所帮助。
修改强> 在断开连接的场景中,你通过线路发送对象来修改,添加,删除对象,我确实定义了一个枚举,它将传递给我发送给客户端的每个Dto /实体,客户端将修改此属性为当您尝试使用Entity框架保存整个图形时,让服务器知道每个对象的状态,因此枚举将如下所示
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 =&gt;中的状态。 EF州,反之亦然。
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");
}
}
}
希望有所帮助。