I manage a publicly available WPF app which uses Entity Framework 6.1 (model-first ObjectContext) and a SQLite database.
The number one crash log (according to HockeyApp) for my app is the following (with pointers to my app's code marked ****):
System.InvalidOperationException: The property 'Id' is part of the object's key information and cannot be modified.
at System.Data.Entity.Core.Objects.EntityEntry.VerifyEntityValueIsEditable(StateManagerTypeMetadata typeMetadata, Int32 ordinal, String memberName)
at System.Data.Entity.Core.Objects.EntityEntry.GetAndValidateChangeMemberInfo(String entityMemberName, Object complexObject, String complexObjectMemberName, StateManagerTypeMetadata& typeMetadata, String& changingMemberName, Object& changingObject)
at System.Data.Entity.Core.Objects.EntityEntry.EntityMemberChanging(String entityMemberName, Object complexObject, String complexObjectMemberName)
at System.Data.Entity.Core.Objects.EntityEntry.EntityMemberChanging(String entityMemberName)
at System.Data.Entity.Core.Objects.ObjectStateEntry.System.Data.Entity.Core.Objects.DataClasses.IEntityChangeTracker.EntityMemberChanging(String entityMemberName)
at System.Data.Entity.Core.Objects.DataClasses.EntityObject.ReportPropertyChanging(String property)
****
at MyNamespace.MyApp.MyEntity.set_Id(Int64 value) in C:\MyPath\MyApp\EfGeneratedEntities.cs
****
at lambda_method(Closure , Object , Object )
at System.Data.Entity.Core.Objects.DelegateFactory.SetValue(EdmProperty property, Object target, Object value)
at System.Data.Entity.Core.Objects.StateManagerMemberMetadata.SetValue(Object userObject, Object value)
at System.Data.Entity.Core.Objects.Internal.LightweightEntityWrapper`1.SetCurrentValue(EntityEntry entry, StateManagerMemberMetadata member, Int32 ordinal, Object target, Object value)
at System.Data.Entity.Core.Objects.EntityEntry.SetCurrentEntityValue(StateManagerTypeMetadata metadata, Int32 ordinal, Object userObject, Object newValue)
at System.Data.Entity.Core.Objects.ObjectStateEntryDbUpdatableDataRecord.SetRecordValue(Int32 ordinal, Object value)
at System.Data.Entity.Core.Mapping.Update.Internal.PropagatorResult.SetServerGenValue(Object value)
at System.Data.Entity.Core.Mapping.Update.Internal.UpdateTranslator.BackPropagateServerGen(List`1 generatedValues)
at System.Data.Entity.Core.Mapping.Update.Internal.UpdateTranslator.Update()
at System.Data.Entity.Core.EntityClient.Internal.EntityAdapter.<Update>b__2(UpdateTranslator ut)
at System.Data.Entity.Core.EntityClient.Internal.EntityAdapter.Update[T](T noChangesResult, Func`2 updateFunction)
at System.Data.Entity.Core.EntityClient.Internal.EntityAdapter.Update()
at System.Data.Entity.Core.Objects.ObjectContext.<SaveChangesToStore>b__d()
at System.Data.Entity.Core.Objects.ObjectContext.ExecuteInTransaction[T](Func`1 func, IDbExecutionStrategy executionStrategy, Boolean startLocalTransaction, Boolean releaseConnectionOnSuccess)
at System.Data.Entity.Core.Objects.ObjectContext.SaveChangesToStore(SaveOptions options, IDbExecutionStrategy executionStrategy, Boolean startLocalTransaction)
at System.Data.Entity.Core.Objects.ObjectContext.<>c__DisplayClassb.<SaveChangesInternal>b__8()
at System.Data.Entity.Infrastructure.DefaultExecutionStrategy.Execute[TResult](Func`1 operation)
at System.Data.Entity.Core.Objects.ObjectContext.SaveChangesInternal(SaveOptions options, Boolean executeInExistingTransaction)
at System.Data.Entity.Core.Objects.ObjectContext.SaveChanges()
****
at MyNamespace.MyApp.MyContext.OnMyEvent() in C:\MyPath\MyApp\MyContext.cs
****
I have not been able to reproduce this crash, and so I have not been able to attempt to fix it. I would love to get it sorted.
I understand the crash message. But my app is not doing anything that should cause this. The MyEntity.Id
property is an entity key (with StoreGeneratedPattern
= Identity
) and I never manually set the Id
value. I never manually mark an entity as modified. My SQLite DB does not have any triggers or anything else that could mess with EF's interpretation of DB-generated identity values. I really can't see how this crash could happen, and I can't make it happen myself - inserts of new entities work just fine for me.
I of course don't expect anyone to read this post and come up with the solution, but I'm hoping somebody can lead me in the right direction.
What non-obvious reasons could cause this crash to happen?
I'm sure I'll be asked for source code, but there's really not much to it. In the culprit code I simply create a MyEntity
object, set some property values, add it to context, and call SaveChanges()
. That's it. As I said, it works just fine in my testing and for 99% of customer usage. But I get several crash reports like this every day and I'd love to put an end to them.
---- UPDATE ----
I think it's worth pointing out that the MyEntity.set_Id(Int64 value)
in the stack trace is clearly part of Entity Framework's key/identity value retrieval (see lines around PropagatorResult.SetServerGenValue(Object value)
, etc.). This is not my code setting the Id
, it's EF.
Looking the Entity Framework source code, this exception is thrown when trying to set an entity key value for an entity not in the Added
state:
// Key fields are only editable if the entry is the `Added` state.
if (member.IsPartOfKey
&& State != EntityState.Added)
{
throw new InvalidOperationException(Strings.ObjectStateEntry_CannotModifyKeyProperty(memberName));
}
So could the issue be that MyEntity is somehow being put into an alternate state (e.g. Modified
) before SaveChanges()
(or during SaveChanges()
but before identity value retrieval)?
How could that happen? As I mentioned, I am not manually setting the state anywhere.
Solved.
The MyNamespace.MyApp.MyContext.OnMyEvent()
line at the bottom of my stack trace gets executed at a scheduled time.
It turns out that it would be possible for two or more executions to be scheduled at the same time. In some cases (even in my testing it was inconsistent and usually worked anyway), it would cause this crash, presumably because SQLite's EF generated ID retrieval could be tripped up when multiple DB inserts are done in near instantaneous proximity.
Anyway, the solution was to simply wrap the code in MyNamespace.MyApp.MyContext.OnMyEvent()
in a lock { ... }
statement to prevent concurrent execution.