Answered by:
AzureTableContext.UpdateObject() fails for entities that implement IEquatable<T>

Question
-
I have an entity that implements TableServiceEntity and for business reasons also implements IEquatable<>. In testing, if I retrieve the object, make a change and then call context.UpdateObject(entity), I get the following error:
System.ArgumentException: The context is not currently tracking the entity.
In experimenting with this, it seems that only the GetHashCode() for the entity is used by the context and the only properties that can be implemented successfully are PartitionKey and RowKey. If anything else is used to generate the hashcode, UpdateObject() throws that error. Of course this does not help us as we need value equals semantics that check multiple properties on the object.
Included is a very simple console app to demonstrate the issue. If you comment out the line for Name in GetHashCode() and un-comment the other two lines, it works.
Is there any way to get value equals semantics with the AzureTableContext? What am I doing wrong? Any help would be appreciated.
using System; using System.Linq; using Microsoft.WindowsAzure; using Microsoft.WindowsAzure.StorageClient; namespace AzureTableContextIEquatableTest { class Program { static void Main(string[] args) { string connStr = "UseDevelopmentStorage=true"; string tableName = "AzureTableContextIEquatableTest"; CloudStorageAccount account = CloudStorageAccount.Parse(connStr); account.CreateCloudTableClient().CreateTableIfNotExist(tableName); A original = new A { PartitionKey = "1", RowKey = Guid.NewGuid().ToString(), Name = "FirstObject" }; TableServiceContext context = new TableServiceContext(account.TableEndpoint.ToString(), account.Credentials); context.AddObject(tableName, original); context.SaveChanges(); context = new TableServiceContext(account.TableEndpoint.ToString(), account.Credentials); A copy = context.CreateQuery<A>(tableName).Where(a => a.RowKey == original.RowKey).First(); copy.Name = "SecondObject"; context.UpdateObject(copy); context.SaveChanges(); //account.CreateCloudTableClient().DeleteTable(tableName); } } class A : TableServiceEntity, IEquatable<A> { public string Name { get; set; } public bool Equals(A other) { if (ReferenceEquals(null, other)) { return false; } if (ReferenceEquals(this, other)) { return true; } return Equals(other.Name, Name); } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) { return false; } if (ReferenceEquals(this, obj)) { return true; } if (obj.GetType() != typeof(A)) { return false; } return Equals((A)obj); } public override int GetHashCode() { return (Name != null ? Name.GetHashCode() : 0); //int result = (PartitionKey != null ? PartitionKey.GetHashCode() : 0); //return (result * 397) ^ (RowKey != null ? RowKey.GetHashCode() : 0); } } }
- Edited by tbpenland Tuesday, June 26, 2012 4:43 PM
Tuesday, June 26, 2012 4:43 PM
Answers
-
Azure table defines a single clustered index for all entities – (PartitionKey, RowKey). This combination, by definition of being a clustered index, needs to provide uniqueness. If you have multiple properties that define uniqueness then one option is to combine these multiple properties as PK or RK. In addition to enable easy access to these properties, you can also define these as separate columns. When combining multiple properties, the prefix property is more important since knowing the value will allow you to restrict the range of the query. The following presentation may help more in understanding how to design (around 1:02:00).
- Proposed as answer by Vitor Faria TomazMicrosoft employee Sunday, July 1, 2012 1:29 PM
- Edited by Jai Haridas Sunday, July 1, 2012 1:30 PM
- Marked as answer by Arwind - MSFT Monday, July 2, 2012 9:12 AM
Sunday, July 1, 2012 1:05 PM
All replies
-
Hi,
As you’ve found out, OData .NET client library’s data context relies on hash code to track entities. So you have to use data service key (in this case, partition and row keys) to compute hash code. This makes sense. After all, even if the name of two entities are the same, as long as the data service keys are different, they should be treated as different entities. If you want to use name to distinguish different entities, put the name in either partition key or row key.
BR,
Arwind
Please mark the replies as answers if they help or unmark if not. If you have any feedback about my replies, please contact msdnmg@microsoft.com Microsoft One Code Framework
Wednesday, June 27, 2012 6:41 AM -
Hi Arwind,
That's the thing though, if two entities have the same name, they are the same entity from a business perspective (value equals semantics is what is important here, not reference equals). If it were just as easy as name, then you are correct that we could use name in one of the keys, but unfortunately there are 6 identified properties that determine uniqueness from a business perspective, so creating a key out of those isn't really feasible.
What appears to be going on is that OData internally is identifying objects based on a list of keys, not the objects themselves. So when I add the object, it calls the object's implementation of GetHashCode and compares it to a hash from its internal list of the PartitionKey and RowKey.
This unfortunately results in a leaky abstraction where the internal details of OData's context impact your object design in unexpected ways.
-Todd
Wednesday, June 27, 2012 5:13 PM -
Hi,
Base on my understanding, this is how OData .NET client library is designed. If you have concerns, you can submit a feature suggestion on http://data.uservoice.com/forums/72027-wcf-data-services-feature-suggestions . I think it makes sense to use key instead of objects. Objects are CLR specific concepts. A CLR object is different from a Java or C++ object. In a storage medium, such as table storage or database, there's no concept of object at all.
Please mark the replies as answers if they help or unmark if not. If you have any feedback about my replies, please contact msdnmg@microsoft.com Microsoft One Code Framework
Thursday, June 28, 2012 3:09 AM -
Azure table defines a single clustered index for all entities – (PartitionKey, RowKey). This combination, by definition of being a clustered index, needs to provide uniqueness. If you have multiple properties that define uniqueness then one option is to combine these multiple properties as PK or RK. In addition to enable easy access to these properties, you can also define these as separate columns. When combining multiple properties, the prefix property is more important since knowing the value will allow you to restrict the range of the query. The following presentation may help more in understanding how to design (around 1:02:00).
- Proposed as answer by Vitor Faria TomazMicrosoft employee Sunday, July 1, 2012 1:29 PM
- Edited by Jai Haridas Sunday, July 1, 2012 1:30 PM
- Marked as answer by Arwind - MSFT Monday, July 2, 2012 9:12 AM
Sunday, July 1, 2012 1:05 PM