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).

January 26, 2010

Quest of the day

What is intended purpose of this class:
  public sealed class ProxyKeyGenerator<TKeyConsumer, TKeySource> : KeyGenerator
    where TKeyConsumer : Entity
    where TKeySource : Entity
  {
    public override Tuple Next()
    {
      var key = Key.Create<TKeySource>(Handlers.Domain);
      return key.Value;
    }
   
    public ProxyKeyGenerator(KeyProviderInfo keyProviderInfo)
      : base(keyProviderInfo)
    {}
  }
I.e. imagine this key generator is practically used somewhere. What can be the purpose of this? :)

A hint: I'm going to write a post related to legacy data import into DO4 database.

January 25, 2010

New chapter in Manual: "Hacker's guide to DataObjects.Net"

Check it out.

One more Manual update

I just fixed a set of major issues related to single-page HTML & PDF version of Manual:
  • Wrong spacing (both PDF and HTML), especially in code examples. That's because single-page (merged) version was produced by our own tool, and this tool really didn't keep all the spaces earlier (there are XDocument-based transformations, so it was pretty easy to face this issue). 
  • No bookmarks in PDF.
  • Page breaks in PDF aren't based on CSS rules. But I finally found a good way of converting everything to PDF.
  • Some links are missing \ wrong. Now everything is really simple: all the interesting links are shown as-is, so you can type them even from printed manual. Of course, they're clickable in PDF version. 
So now Manual is fully ready for printing. That's really cool - actually a lot of my time was spent on achieving this.

Left issues (except content-related ones):
  • Table of contents does not include page numbers. It seems I'll be unable to fix this automatically. 
  • Its size is 9Mb now I'll try to reduce it later ;)
Now I'm working on its inclusion into .HxS / .Chm - it must be split to topics (up to <h2> headers) to appear there ideally.

January 21, 2010

Why there is no [DebuggerSkipThrownException(typeof(...))]?

That's really annoying. In some cases catching an exception without re-throwing it is the best way to implement specific case handling. E.g. one of our IoC related types, AssociateProvider, uses such a code with Activator.CreateInstance trying to create instances of candidate types sequentially (the sequence is defined by a set of rules):
  • If something goes wrong, it simply tries the next one.
  • If it finds the appropriate type (i.e. the one which instance was successfully created), it caches this info and does not use sequential search process for it further.
  • If there is no such a type, it finally returns null (no associate was found).
But since there is no overload of Activator.CreateInstance allowing to get e.g. null instead of created type in case of error, we should:
  • Either catch & suppress some exceptions it throws. That's what we have now.
  • Or manually check all the preconditions, which is pretty complex. We already check some of them to minimize the count of thrown exceptions, but there are still some pretty complex cases where exceptions are thrown an caught. Typical exception message we have there now is "GenericArguments[0], 'System.Object', on 'Xtensive.Core.Comparison.ObjectComparer`1[T]' violates the constraint of type parameter 'T'.". So to avoid getting this exception, we must check generic arguments - their count constraints.
Obviously, second case seems ridiculous: .NET Framework already contains the code that can do this, and it works. But:
  • This code throws an exception in case of error
  • Visual Studio .NET does not provide  a good way to suppress it (i.e. behave like it is normal control flow).
That's why I really miss [DebuggerSkipThrownException(typeof(XxxException))] attribute. Imagine, when it is applied to some method, an exception of XxxException type thrown in its stack frame or anywhere above on stack must not lead to break in Visual Studio .NET. If we have this, it could be an ideal solution of such problems.

But since Microsoft doesn't care much about this (although I hope it will), we must implement our own, non-ideal one - which will make the code much less clear...

January 12, 2010

Preliminary version of DataObjects.Net Manual in PDF

I've just published preliminary version of DataObjects.Net Manual in PDF.

Do you know a good way or tool allowing to convert HTML with links and paging martup (CSS attributes) to PDF? Actually I didn't thought this is so complex:
  • Adobe Acrobat seems relying on Internet Explorer, that actually understand basic paging rules. But when Acrobat converts the page, somehow it makes IE to simply ignore any paging  attributes in CSS.
  • Google Chrome and Mozilla don't properly handle paging attributes.
  • New Opera ideally prints everything (in particular, to PDF printers). But I can't convert HTML links to PDF links by this way. So should I simply replace them to printable ones in Manual? And, obviously, there is no automatic way to convert TOC links as well. 
  • All online converters I tried produce even worse results. 
So I'd appreciate any advices related to conversion of HTML to PDF.

January 6, 2010

DisconnectedState, OperationSet, events and operation logging

There is a discussion at our Support forum, where lots of concepts related to DisconnectedState (and related aspects - e.g. Session events) are explained. Another one explains what features will be supported there. If you're interested in this feature, please add your own questions. Shortly I'll be writing a Manual section covering this feature, so good questions here would help me a lot.

P.S. Here are long New Year holidays till January 10, so that's why there is almost full silence in our blogs. But some works are going on - this must be visible in update log.

January 1, 2010

Happy New Year!

We're already celebrating it in Ekaterinurg ;)

P.S. Yesterday I finished "Getting ideal performance" article in Manual - it covers lazy loading, prefetch API, future (delayed) queries and statement batching in DataObjects.Net. Comments are very appreciated.