Show Blogger Panel Hide Blogger Panel
Alex Yakunin

January 28, 2010

The purpose of ProxyKeyGenerator

Few days ealier I posted a kind of quiz, and here is the answer to it.

Let's have a look at example code using it:
  [Serializable]
  [HierarchyRoot]
  public class Author : Entity
  {
    [Key, Field]
    public int Id { get; private set; }

    [Field(Length = 200)]
    public string Name { get; set; }

    [Field]
    public EntitySet<Book> Books { get; private set; }

    public override string ToString()
    {
      return Name;
    }

    // Constructors

    public Author()
    {
    }

    public Author(int id)
      : base(id)
    {
    }
  }

  [Serializable]
  [HierarchyRoot]
  [KeyGenerator(typeof(ProxyKeyGenerator<Book, Author>))]
  public class Book : Entity
  {
    [Key, Field]
    public int Id { get; private set; }

    [Field(Length = 200)]
    public string Name { get; set; }

    [Field]
    [Association(PairTo = "Books")]
    public EntitySet<Author> Authors { get; private set; }

    public override string ToString()
    {
      return string.Format("{0} by {1}",
        Name, Authors.ToCommaDelimitedString());
    }

    // Constructors

    public Book()
    {
    }

    public Book(int id)
      : base(id)
    {
    }
  }

  [TestFixture]
  public class ProxyKeyGeneratorTest
  {
    [Test]
    public void CombinedTest()
    {
      // Creating new Domain configuration
      var config = new DomainConfiguration("sqlserver://localhost/DO40-Tests") {
        UpgradeMode = DomainUpgradeMode.Recreate
      };
      // Registering all types in the specified assembly and namespace
      config.Types.Register(typeof (Author).Assembly, typeof(Author).Namespace);
      // And finally building the domain
      var domain = Domain.Build(config);

      using (Session.Open(domain)) {
        using (var transactionScope = Transaction.Open()) {

          // Creating two authors
          var joseph = new Author {Name = "Joseph Albahari"};
          var ben    = new Author {Name = "Ben Albahari"};
          
          // Creating the Book book with book.Id = joseph.Id
          var book = new Book(joseph.Id) {Name = "C# 4.0 in a Nutshell"};
          book.Authors.Add(joseph);
          book.Authors.Add(ben);

          // Testing ProxyKeyGenerator
          Assert.AreSame(joseph, Query.SingleOrDefault(joseph.Key));
          Assert.AreSame(ben, Query.SingleOrDefault(ben.Key));
          // Must fail, if [KeyGenerator(typeof(ProxyKeyGenerator<Book, Author>))]
          // line is commented
          Assert.AreSame(book, Query.SingleOrDefault(book.Key));

          // Let's finally print the Book 
          Console.WriteLine(book);

          transactionScope.Complete();
        }
      }
    }
  }

In this example we manually assign Book key that is equal to Author key - imagine this is really necessary, e.g. we import the data and want to keep old Id values, or we simply deal with legacy schema using DomainUpgardeMode.Legacy. By default (i.e. if there is no [KeyGenerator(typeof(ProxyKeyGenerator<Book, Author>))] line) such an attempt must lead to completely unexpected result: the test fails, because an attempt to read this Book by its key returns nullBut why?

ProxyKeyGenerator used in this example actually affects just on Key comparison. DataObjects.Net compares Key objects using two values they store:
  • KeyProviderInfo object (from storage model). Each KeyProviderInfo object has 1-to-1 relationship with KeyGenerator (the object that is actually responsible for key generation).
  • Key value (Tuple, or simply a value of particular key type - I'll explain this below).
See e.g. LongKey.GetHashCode() method code - it fully explains this. LongKey is one of actual Key implementations we use (the generic one). Others are Key<T>, Key<T1,T2>, ... Key<T1,T2,T3,T4> (later we may add other similar ones) - lightweight versions of Key used in most frequent cases to increase performance.

Now few more important facts:
  • There is a single KeyProviderInfo object (and thus KeyGenerator object) created for each KeyGenerator type referenced from your model.
  • If custom key generator isn't bound to a particular hierarchy, its default implementation (of XxxKeyGenerator<TFieldType> type) is provided by current storage provider. E.g. SqlCachingKeyGenerator<TFieldType> is used by any SQL storage provider.
So as you see, two keys from different hierarchies can be equal, if they share the same key value and the same key generator type. These keys are actually indistinguishable from the point of DataObjects.Net, i.e. both keys are nothing more than different instances of key of the same entity. So any subsequent attempt to resolve any of these keys will lead to the same result as it was in the first attempt.

E.g. if you'd try to resolve Book key first, any subsequent attempt to do the same with Author key with the same value will anyway return Book (if expected entity type is not specified), and vice versa. This happens because key-entity pair gets cached after the first database lookup (btw, database lookup will rely on actual key type that is actually stored with key), and any subsequent cache lookup with virtually the same key will succeed further.  But since we use Query.SingleOrDefault<T>(...) in our test, it returns null.

Finally, the most important question:

Why key comparison is so strange?

Short answer is: such behavior is required to support persistent interfaces.

Imagine we have IPerson interface, and one more type support it; Book.Persons is EntitySet<IPerson> now.

Persistent interface require any persistent type implementing it to:
  • Use the same key type as it's specified in persistent interface
  • Belong to a hierarchy sharing the same KeyGenerator as any other hierarchy where implementors of this persistent interface exists. 
I.e. all implementors of persistent interface must share the same key type, and moreover, the same key generator.
  • First condition ensures it is possible to map reference to persistent interface to relational structure. I.e. we know what columns must be created to describe such a reference.
  • Second condition ensures identity of key value through all persistent interface implementations. If there is no such condition, two indistinguishable references to different IPerson instances could exist in the database.
Such set of condition in conjunction with described way of key comparison allows us (and you) to successfully execute e.g. this code: Query.SingleOrDefault<IPerson>(Key.Create<IPerson>(value)). Btw, nearly this code is actually used to return reference property value (e.g. IPerson instance) - as you know, internally DataObjects.Net stores any reference as a set of its key values inside Tuple representing entity state.

P.S. I understood we must add explicit assertions to Query.Single* method group throwing an exception with appropriate message when a key belonging to hierarchy X is resolved to entity from hierarchy Y (this is possible only in case with cache hit). In fact, it indicates you either must use ProxyKeyGenerator type, or fix your data import routines assigning the keys manually (or simply working without DataObjects.Net at all).