0

When I try to add an entity with new children, I get InvalidOperationException in the EntityFramework.dll.

I have set a small test app to attempt to understand this issue.

I have two models: Parent and Child.

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; }
}

at the WebAPI side I have a controller 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);
    }

I have thrown together a WPF app to exercise the API.

On a Button click:

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 looks like:

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 and ApiConsumerResponse look like:

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 (which is not a factory) and it's response class look like:

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; }
}

So all good. Testing the Get Method (not shown) it returns parent entities - GOOD.

The problem I have is when I try to 'PUT' a parent entity with a new child entity. As Shown in the Button_Click method.

The Parent entity with the new child arrives at the parentController however when I try to set state as modified:

db.Entry(parent).State = EntityState.Modified;

The Error is thrown: A referential integrity constraint violation occurred: The property value(s) of 'Parent.ParentId' on one end of a relationship do not match the property value(s) of 'Child.ParentId' on the other end.

Now as a test I changed out the PUT method on the controller To emulate the attempt from the client.

Modified PUT method:

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());
     }

Which works perfectly. The Child gets added to the DB it's ParentID is updated and it's own key is generated.

So why does the object that comes across the wire blow up EF?

I tried attaching the object first (db.Parents.Attach(parent);) but that throws the same error.

Im confused.

John
  • 1,714
  • 21
  • 41

1 Answers1

1

Entity Framework needs to track the objects to know which goes where and generates the SQL query accordingly, and part of this is done by you, by setting the State of the object, so if you set the parent's state to be modified but the new child's state was not set to be Added (Default is Unchanged), the entity frameowork here will treat this object as already exists in memory, and this is not the case.

but when added the child to the list of the children within the API the entity framework will set the Child's state to be Added, and will generate the SQL to insert the new child and link the ids accordingly.

Hope that helps.

EDIT In the case of disconnected scenario, where you send the objects across the wire to modify, add, delete objects, I do define an enum that will pass with each Dto/entity I send to the client, and the client will modify this property to let the server knows what the status of each object when you try to save the whole graph with Entity framework, so the enum will look like this

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
}

and then I define a method that will translate this enum value to the entity' state that the entity framework knows about, my method will have something like this

// 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;
            }
        }

And finally this is my helper class to convert the states from my ObjectState => EF State, and Vise versa.

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");
        }
    }
}

hope that helps.

Omar.Alani
  • 4,050
  • 2
  • 20
  • 31
  • Thank you for the steer. I now find my self reading Programming the Entity Framework DbContext by Julia Lerman and Rowan Millar. Among other strategies they discus a very similar approach to the one you have provided here. Currently I am investigating: Trackable Entities: N-Tier Support for Entity Framework by Tony Sneed available via codplex or the visual studio gallery. – John Oct 29 '14 at 14:59