"Attaching an entity of type T failed because another entity of the same type already has the same primary key value"

c# entity-framework entity-framework-6

Question

I possessLanguage model as such a definition:

public class Language
{
    [JsonProperty("iso_639_1")]
    public string Iso { get; set; }

    [JsonProperty("name")]
    public string Name { get; set; }

    public override bool Equals(object obj)
    {
        if (!(obj is Language))
        {
            return false;
        }

        return ((Language)obj).Iso == Iso;
    }

    public override int GetHashCode()
    {
        return Iso.GetHashCode();
    }
}

This is modeled using.Movie as ICollection<Language> SpokenLanguages . I'm using the data I collect to seed my database. When several films have the same language, I want to reuse the current entry in theLanguages table.

By combining new types with previously existing ones, the following accomplishes that:

var localLanguages = context.Languages.ToList();
var existingLanguages = localLanguages.Union(movie.SpokenLanguages);
var newLanguages = localLanguages.Except(existingLanguages).ToList();
newLanguages.AddRange(existingLanguages);
movie.SpokenLanguages = newLanguages;

Although it functions, this is undoubtedly unsightly and not EF-friendly. I'm trying to tie the current models to EF so that it would automatically re-use them, but I'm having trouble getting it to work. I keep getting the following error message:

Attaching an entity of type 'Models.Movies.Language' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.

This is the code in question:

var localLanguages = context.Languages.ToList();
foreach (var language in movie.SpokenLanguages)
{
    if (localLanguages.Contains(language))
    {
        context.Languages.Attach(language);
        // no difference between both approaches
        context.Entry(language).State = EntityState.Unchanged;
    }
}

establishing that stateUnchanged or Modified does not affect anything. The JSON response I receive is

{
    "iso_639_1": "en",
    "name": "English"
}

Both of these values match those that are already present in the database exactly.

Every insertion into the database both produces and discards a new context.

How can I avoid having to sort through the current language entries and have EF use them instead?

1
3
2/16/2015 3:44:31 PM

Accepted Answer

I changed the model so that it now has a field.Id and make that the main key. Everything else has remained the same, including equality comparisons. I now get a different error notice that might provide further information about the problem:

{"The INSERT statement conflicted with the FOREIGN KEY constraint "FK_dbo.MovieLanguages_dbo.Languages_LanguageId". The conflict occurred in database "MoviePicker", table "dbo.Languages", column 'Id'. The statement has been terminated."}

Additional information: An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details.

The last SQL statement that was executed, which I recorded in the datacontext, was as follows:

INSERT [dbo].[MovieLanguages]([MovieId], [LanguageId])
VALUES (@0, @1)

-- @0: '2' (Type = Int32)  
-- @1: '0' (Type = Int32)

This demonstrates theLanguageId (field Id in a deskLanguage ) is not filled in. Its default value is 0, and the only thing I do with it is attach it to the EF configuration, so this makes sense. Because it is attempting to establish a reference to an entry with ID 0 that does not already exist, this does not prompt it to assume the value of the existing object, leading to an FK constraint error.

Knowing this, I tried to combine my strengths with my objectives. I start by checking to see if the language is present in the database. If not, everything continues as normal, and I just insert it. If it's already there, I give the new item its ID if it is.Language item, remove the old one, then reattach the new one.

The object that EF maintains track of was essentially switched. This is the best I could come up with; it would have been really great if it would automatically do this when it detects object equality.

var localLanguages = _context.Languages.ToList();
foreach (var language in movie.SpokenLanguages)
{
    var localLanguage = localLanguages.Find(x => x.Iso == language.Iso);

    if (localLanguage != null)
    {
        language.Id = localLanguage.Id;
        _context.Entry(localLanguage).State = EntityState.Detached;
        _context.Languages.Attach(language);
    }
}
1
2/17/2015 3:06:55 PM

Popular Answer

Consider using theIEquatable<T> connect on yourLanguage entity, I supposeIso is the primary key for the entity):

public class Language : IEquatable<Language>
{
    [JsonProperty("iso_639_1")]
    public string Iso { get; set; }

    [JsonProperty("name")]
    public string Name { get; set; }

    public override bool Equals(object obj)
    {
        return Equals(other as Language);
    }

    public bool Equals(Langauge other)
    {
        // instance is never equal to null
        if (other == null) return false;

        // when references are equal, they are the same object
        if (ReferenceEquals(this, other)) return true;

        // when either object is transient or the id's are not equal, return false
        if (IsTransient(this) || IsTransient(other) ||
            !Equals(Iso, other.Iso)) return false;

        // when the id's are equal and neither object is transient
        // return true when one can be cast to the other
        // because this entity could be generated by a proxy
        var otherType = other.GetUnproxiedType();
        var thisType = GetUnproxiedType();
        return thisType.IsAssignableFrom(otherType) ||
            otherType.IsAssignableFrom(thisType);
    }

    public override int GetHashCode()
    {
        return Iso.GetHashCode();
    }

    private static bool IsTransient(Language obj)
    {
        // an object is transient when its id is the default
        // (null for strings or 0 for numbers)
        return Equals(obj.Iso, default(string));
    }

    private Type GetUnproxiedType()
    {
        return GetType(); // return the unproxied type of the object
    }
}

Try it again with this:

var localLanguages = context.Languages.ToList(); // dynamic proxies
foreach (var language in movie.SpokenLanguages) // non-proxied
{
    if (localLanguages.Any(x => x.Equals(language)))
    {
        context.Entry(language).State = EntityState.Modified;
    }
}

I'm wondering if the fact that EF employs dynamic proxies for entity instances that are loaded from the context would affectContains was returning in an unexpected way.false value. I think thatContains merely perform a reference comparison; not an actualEquals comparison. Given that the context's entities are dynamic proxy instances and yourmovie.SpokenLanguages do not,Contains possibly not been comparable as you anticipated.

Citation: https://msdn.microsoft.com/en-us/library/ms131187(v=vs.110).aspx

The IEquatable interface is used by generic collection objects such as Dictionary, List, and LinkedList when testing for equality in such methods as Contains, IndexOf, LastIndexOf, and Remove. It should be implemented for any object that might be stored in a generic collection.



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow