I’ve got an azure table record object defined as
[DataServiceKey("PartitionKey", "RowKey")]
public class TableRecord
{
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public DateTime Timestamp { get; set; }
public string Data { get; set; }
}
The record is used as part of the repository infrastructure which accepts business logic level data object and serializes it into the Data property before saving the code to the table storage as well as deserializes it before returning to the client, so the business logic doesn’t know anything about the record as well as about PartitionKey and RowKey.
Here’s the repository method
public TEntity RegisterSave<TEntity>(TEntity entity, bool createNew)
{
var storeRec = _strategy.GetStoreRecord(entity);
if (createNew)
_context.AddObject(storeRec.TableName, storeRec.Record);
else
{
try
{
_context.AttachTo(storeRec.TableName, storeRec.Record, "*");
}
catch (InvalidOperationException)
{
// AttachTo can throw an exception if the entity is already being tracked.
// Ignore
}
_context.UpdateObject(storeRec.Record);
}
return entity;
}
The _strategy is responsible for correctly mapping the entity type to the table name as well as properly creating TableRecord with keys and serializing the entity into the record. The storeRec.Record property is the instance of the TableRecord class. This approach works well for creating new record and reading record.
But when I try to update existing record with the new data the update fails complaining that it cannot update entity which is not tracked by the context. Although if to step through the code in the debugger it turns out that there are two exceptions actually happening – first in the AttachTo method which complains about the entity with the same key is being tracked, and immediately after that the UpdateObject complains about it’s not.
Where did I go wrong?
Got it
Ok, with a little help from ilspy I’ve found the root cause of the issue. The DataServiceContext maintains two dictionaries for the entities loaded to the context. Keys of one dictionary is entity itself, and key of another one is entity id which is essentially the entity url. In AttachTo method the context verifies both dictionaries and throws InvalidOperationException if entry found in any one of them. But UpdateObject method verifies only dictionary where key is entity itself, and fails if not found.
It appears DataServiceContext assumes that modifications can only be done to the same entity, it doesn’t by default support that the entity will be replaced by the new instance as a whole. But the logic uses standard dictionary class with default comparer so after implementing IEquatable interface for the TableRecord everything worked perfectly.
So for me the solution was:
[DataServiceKey("PartitionKey", "RowKey")]
public class TableRecord: IEquatable<TableRecord>
{
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public DateTime Timestamp { get; set; }
public string Data { get; set; }
public bool Equals(TableRecord other)
{
if (other == null)
return false;
return PartitionKey.Equals(other.PartitionKey) && RowKey.Equals(other.RowKey);
}
public override bool Equals(object obj)
{
return Equals(obj as TableRecord);
}
public override int GetHashCode()
{
return PartitionKey.GetHashCode() ^ RowKey.GetHashCode();
}
}
The solution to this is that if there’s an existing entity, you need to Detach() it and the AttachTo() your new one. Then do the updates you want to do.
I wrote some code that does this. It also avoids throwing an exception, though I don’t know for sure which method is faster.
To use this, you would replace your try/catch block with a call to
SafeAttach(storeRec).Note that this approach defeats the built-in concurrency checking of table storage. You’re basically getting last-write-wins behavior. That may or may not be acceptable, it depends on your situation.
Also, if this is how you plan to continue working, you may want to set MergeOption.NoTracking. You’re basically emulating that behavior anyway, and there is some performance advantage to disabling entity tracking.