Fluent Interface for System.Identity – M, Persistence, & Equals()
As I wrote in Part 2 of the series, System.Identity ships in the latest Oslo CTP as part of the Oslo Repository. This has the direct effect of introducing ORM database persistence considerations into the design of the System.Identity fluent interface I’ve been crafting.
One of these considerations has already manifested in the following snippet of seemingly straightforward code:
1: public bool IsOwnedBy ( Party owner )
2: {
3: if ( Owner == null || owner == null )
4: {
5: return false;
6: }
7: else
8: {
9: return Owner.Equals ( owner );
10: }
11: }
In the snippet above, line #9 evaluates the equality of the object’s Owner property (as an instance of the Party class) vis-à-vis the passed-in owner parameter.
As it turns out, the Equals() and GetHashCode() methods become quite interesting when dealing with objects that are dehydrated to, and rehydrated from, a database. The subject of this post will be outlining an implementation of Equals() that will work correctly with the fluent interface and a persistence mechanism (e.g., NHibernate) that might be used to store and retrieve objects from the Oslo Repository. My next post will tackle an similar implementation of GetHashCode().
It’s All About the Id
While Oslo’s M language can be used as a straight-up textual modeling language, M has been deliberately designed to make using a SQL Sever database as a model repository incredibly easy. In line with this design goal, M supports a number of language features that directly enable model persistence in SQL Server.
From the M for the Oslo Repository in the May CTP, I offer the HasFolderAndAutoId.m file:
1: //-----------------------------------------------------------------------------
2: // Copyright (c) Microsoft Corporation. All rights reserved.
3: //-----------------------------------------------------------------------------
4:
5: module System
6: {
7: export HasFolderAndAutoId;
8:
9: // Mixin used to include a SQLServer-managed numeric identifier and a folder reference.
10: // Use this mixin when defining normal content types or extents.
11: type HasFolderAndAutoId : HasFolder
12: {
13: Id : Integer64 => AutoNumber();
14:
15: } where identity Id;
16:
17: }
The HasFolderAndAutoId is used as a mixin by all the extents within the System.Identity schema and clearly illustrates that the Id field is used to store a SQL Server-managed identity field. This has particular ramifications when considering what it means for one object instance to be equal to another in the fluent interface.
At base, the problem stems from the fact that the primary means of establishing equality between two System.Identity objects at runtime is the value of the Id field and not the memory address of the object instances – the same row in a database table can be instantiated as more than one object in memory. Since establishing this ‘database equivalence’ is not the default behavior of the Equals() method inherited from System.Object, some custom code is required to ensure that the fluent interface works correctly.
Just to make things a little more interesting, a System.Identity object’s Id field is not actually populated with a value until the first time the object is saved to SQL Server. However, at runtime we may very well have legitimate need to call the Equals() method before the objects are saved. As such, we need an Equals() implementation that handles both states – persisted objects (with Id values) and non-persisted objects (no Id values assigned yet).
To address the no Id situation I’ll just make the Id field of the SystemIdentityBase class (which the Kind class inherits from) a nullable field. Here’s the updated code:
1: public abstract class SystemIdentityBase
2: {
3: protected Int64? Id { get; private set; }
4:
5: // Rest of code omitted
6: }
Handling Reflexivity
OK, with that taken care of, I can stub in some of the no-brainer code for the Equals() method implementation. For those readers that are interested, the book “Effective C#” has excellent coverage of overriding the Equals() method. The code that follows borrows heavily from the book’s goodness:
1: public override bool Equals ( object obj )
2: {
3: if ( obj == null )
4: {
5: return false;
6: }
7:
8: if ( ReferenceEquals ( this, obj ) )
9: {
10: return true;
11: }
12:
13: if ( this.GetType () != obj.GetType () )
14: {
15: return false;
16: }
17:
18: throw new NotImplementedException ();
19: }
Lines 3-11 above are textbook C#, but the code on line 13-16 is interesting. If you take a look at MSDN’s coverage of overriding Equals() there’s this interesting little tidbit:
- x.Equals(y) returns the same value as y.Equals(x).
This has some interesting ramifications on the implementation of the Equals() override. The bullet above prescribes that overrides of Equals() have to support reflexivity. When one considers polymorphic inheritance, this gets a little complicated. Take, for example, two classes where one class (call it ‘B’) inherits from another (call it ‘A’). Using these hypothetical classes, the bullet above prescribes that the instance call of ‘a.Equals(b)’ must return the same value as the instance call of ‘b.Equals(a)’. This example illustrates that 100% reflexivity cannot be provided unless we consider only exact types in our Equals() overrides (again, take a look at “Effective C#” for great coverage of this topic).
The code in lines 13-16 ensures that only objects that are of the exact same type continue on for further Equals() processing.
Addressing Database Equivalence
Given the starting code above, I can add the following to the Equals() method:
1: Kind otherKind = obj as Kind;
2:
3: if ( Id.HasValue && otherKind.Id.HasValue && ( Id.Value == otherKind.Id.Value ) )
4: {
5: return true;
6: }
Here we address the ramifications of M-based persistence in SQL Server. Essentially, the code on line 3 in the snippet above says that two Kind objects in memory are equal if they have the same SQL Server-controlled Id (which implies that both have non-null Id fields).
No rocket science here.
The End Game
Lastly, the Equals() method handles the case where two non-null Kind objects are being compared, the two objects are at different memory addresses, and one of them (or both) doesn’t have an Id populated by SQL Server. Here’s the last of the code:
1: return Name == otherKind.Name &&
2: Keywords. == otherKind.Keywords &&
3: Owner.Equals ( otherKind.Owner );
The code snippet above clearly illustrates that I’m taking the point of view that two Kind object are equal if the Name, Keywords, and Owners are equal (given the constraints listed immediately above the snippet).
This is purely a call on my part since System.Identity does not (to my knowledge) provide any guidance on the equivalence of the extents defined in the schema.
Just for the sake of completeness, here’s the full method in a single listing:
1: public override bool Equals ( object obj )
2: {
3: if ( obj == null )
4: {
5: return false;
6: }
7:
8: if ( ReferenceEquals ( this, obj ) )
9: {
10: return true;
11: }
12:
13: if ( this.GetType () != obj.GetType () )
14: {
15: return false;
16: }
17:
18: Kind otherKind = obj as Kind;
19:
20: if ( Id.HasValue && otherKind.Id.HasValue && ( Id.Value == otherKind.Id.Value ) )
21: {
22: return true;
23: }
24:
25: return Name == otherKind.Name &&
26: Keywords. == otherKind.Keywords &&
27: Owner.Equals ( otherKind.Owner );
28: }
Next Time
As I mentioned above, the subject of my next post will be an implementation of GetHashCode() that will address the ramifications of M-based persistence in SQL Server.
Stay tuned!
Comments
Anonymous
July 22, 2009
Hi Dave, Why are you using a value comparison as the last resort? In your case, either Only one of the IDs is null --> One is persisted, the other is not. Can not be the same thing. As soon as I persist the new object, it would receive a new Id and then the comparison would be different as it is now? Sounds not real world to me. or both Ids are null --> you just created to new objects. You probably have a reason to create two new things and you did not mean them to be the same. Again your solution does not seem to be real world. But still thanks for getting my attention to the party model, even if it is quite close to the "all things model" (which would just require you to name party -> object and to add a table of properties).Anonymous
July 22, 2009
@buzina If I understand your question correctly, the code makes the assumption that there is a business requirement that if two Kind objects are semantically equivalent (as embodied in the return statement), then they are the same - even if there Id's are different. Essentially, the code ignores DB uniqueness (aka the Id as primary key) for non-equal scenarios, and relies on object data instead. In my experience I would disagree that this isn't a real-world scenario. Not really common, but there in the real world. Does this address your question? Thanx for reading! Dave