Show Blogger Panel Hide Blogger Panel
Alex Yakunin

October 30, 2009

Weird issue with accessing Web.config section on 64-bit Windows Server 2008 (IIS 7)

I was returning to this issue for several times starting from this weekend. Initially it was looking really simple: by some reason our ASP.NET sample (as well as ASP.NET MVC and Astoria samples) were simply not working on 64-bit Windows Server 2008 with IIS 7, although everything was fine on Vista. "Not working" means they were showing an error page on attempt to open any of these web application:
  • InvalidOperationException: Section 'Xtensive.Storage' is not found in application configuration file.
It was thrown by our own DomainConfiguration.Load(...) method - the following code there was throwing an exception:

var section = (ConfigurationSection) ConfigurationManager.GetSection(sectionName);
if (section==null) 
  throw new InvalidOperationException(string.Format(
    Strings.ExSectionIsNotFoundInApplicationConfigurationFile, sectionName));

So this was looking pretty simple: by some reason IIS does not see our section and thus returns null from GetSection method.

Certainly, initially I suspected there is something wrong with Web.config file. So I studied everything related to new configuration features that appeared in IIS 7. Nothing helped. I tried to rename this section, wrap it into group and so on. No results.

My Web.config was looking like that:

...
<configSections>
  <section name="Xtensive.Storage" type="Xtensive.Storage.Configuration.Elements.ConfigurationSection, Xtensive.Storage, Version=1.0.0.0, Culture=neutral, PublicKeyToken=93a6c53d77a5296c"/>
   ...
</configSections>
...
<Xtensive.Storage>
  <domains>
    <domain name="mssql" upgradeMode="Recreate" connectionUrl="sqlserver://localhost/DO40-Tests"/>
  </domains>
</Xtensive.Storage>
...

Btw, what was really strange is that actually IIS was seeing this section: when I tried to modify e.g. its name in <section name="..." .../> tag without modifying its tag names below, IIS was showing appropriate (different) error message. Moreover, running the same application under Visual Studio .NET (i.e. under integrated Cassini) on the same PC was leading to no errors!

Finally I found a workaround:

ConfigurationSection section;
if (HttpContext.Current!=null) {
  // See http://code.google.com/p/dataobjectsdotnet/issues/detail?id=459
  // (workaround for IIS 7 @ 64 bit Windows Server 2008)
  var config = WebConfigurationManager.OpenWebConfiguration("~");
  section = (ConfigurationSection) config.GetSection(sectionName);
}
else
  section = (ConfigurationSection) ConfigurationManager.GetSection(sectionName);
if (section==null) 
  throw new InvalidOperationException(string.Format(
    Strings.ExSectionIsNotFoundInApplicationConfigurationFile, sectionName));

Can you imagine this?
  • Why default ConfigurationManager.GetSection does not work the same way in this case? Worse, why it does not fail at all on Vista, but fails only on 64-bit Windows Server 2008?
  • Why, at least, it does not fail properly, with error like "you're using wrong method"? It behaves as if it deals with wrong .config file there.
I spent a lot of time on this mainly because I didn't expect there can be something related to API calls at all. All the problems I faced before on this OS were much more obvious. But in this case I was thinking there is really something wrong with Web.config content until I understood there can be an issue related to GetSection method we call. This was really not obvious for me, since the same code was working properly everywhere else.

Moreover, I couldn't find anything related to this issue on the web. I tried this workaround not because I found it somewhere, but because I found this part of API (I mean OpenWebConfiguration), and, btw, tried few other ways of its usage until it started to work, so possibly GetSection does not work properly not because it is buggy, but because WebConfigurationManager is buggy. Further I discovered there are some known bugs - e.g. I voted for this one. But as I said, I could find nothing exact.

So that's why I wrote this post. The issue seems important, since it is related to generally any web application using custom Web.config section. Of course, if it has any chance to run on 64-bit Windows.

P.S. This is the last bug related to running DataObjects.Net on 64-bit Windows I planned to fix till officially confirming "it works on 64-bit Windows" :)

October 26, 2009

Functional programming, monads: links


Recently I started paying much more attention to functional programming - this happened after a long talk with one really clever guy at our city's developers forum. Let's call him "Amateur Programmer" (this is his real nickname translation; I'll use "AP" to refer to him further). Certainly, he isn't amateur at all, moreover, he made few good talks at our users group. So... We've been discussing lots of topics related to software development in "is this good or bad" fashion, and our view was quite different on almost any different point. I'll list few examples:
  • I think granting the people a simple way of developing their own DSLs is generally bad. Not because DSLs are unnecessary, but because such an opportunity may lead to an explosion of count of DSLs in applications, and consequently, to explosion of count of absolutely crazy ways of writing programs (language design is complex!). So e.g. I don't pay much attention to Oslo's M, as well as JetBrains' MPS. Obviously, not because they're useless, but because they are intended for pretty small subset of developers and companies. In short, I like the beauty of C# (with lots of exceptions :) ), and generally I would prefer to avoid dealing with any home-made language. AP was strongly protecting DSLs.
    Side note: later I understood we're actually talking and even thinking about the same, but our origin points are different. So as it frequently happens, the truth was in the middle.
  • I'm OOP fan. AP is FP (functional programming) fan; moreover, he generally exposed a point that OOP is wrong way of software development at all. That was pretty strange, because I knew he programs mainly on C#. On the other hand, my view on FP was nearly the following: "it must be used when it really matches the task best; but I can't admit I'd use it without OOP practices". Moreover, I clearly understand that e.g. concurrent programming is where FP looks is the best match.
    Side note: the same :)
There were other less interesting topics - e.g. we've been discussing RUP, UML, complexity of LINQ provider implementation and so on. What's the most important is that he gave a link to monadic parsers combinators in C#. I read this article and felt there is really something new for me. In fact, I felt there is a very generic concept (monad) I know almost nothing about. Moreover, we touched HaskellDB (AP's point was: HaskellDB is in fact, LINQ analogue in Haskell, and since it is just 200Kb, implementing LINQ translator must be much simpler in Haskell, so FP rules). What was really important for me here is that I discovered that Erik Meijer (known as architect of LINQ) was one of its co-authors - take a look at slide 6 here.

So this was enough for me to start looking up everything related to monads, F#, Haskell and FP in general. I was even astonished a bit I was ignoring this field for all the years I graduated from the university. That's what takes some of my free time during last 2 weeks ;)

My current conclusions are:

1. FP is what you must start studying right now - I think clearly understanding monads is what I'd call minimum here. Speaking shortly, FP times have came. Its upcoming application areas:
  • Asynchronous programming. Likely, you know, that pure FP programs are intrinsically concurrent, because there are no side effects. Take a look as async monad in F# as well. If you aren't sure if async programming is really important, think what's increasing during last 5 years: the count of CPU cores, or CPU frequency. Free lunch is over.
  • Monads. They're native for FP languages, and provide really powerful abstraction for dealing with side effects safely. I was quite surprised to know that e.g. IEnumerable<T> is just a particular implementation of monad. Since I'm not feeling myself fully comfortable in this area, I'll just give you some links related to further. But I can promise you it will be at least really interesting to study this.
  • Integrated DSLs. Functional style favors building DSLs integrated into a "host language" (i.e. fully based on its syntax). You may look at LINQ as DSL built for dealing with sequences in C#; Rx Framework is the same DSL built for events. You shouldn't build neither a king of execution platform for such a language, nor compiler, its libraries and so on, but you may use all the stuff offered by the host platform there. Obviously, that's great. That's the "truth in the middle" about DSLs I was talking about :)
2. Haskell is highly recommended language to start from - every FP guru talks mainly on this language, so you'll need it to understand even C9 videos related to FP and monads ;)

3. F# also worth studying - at least because it is primary functional language on .NET. It is quite close to pure FP languages by offered features (but it can interact with imperative ones via .NET platform), but nearly as fast as C#. Likely, you won't need it on practice - C# is "functional enough", and as all of us seen, it constantly moves to functional direction (btw, that's one more reason to study FP fundamentals). Likely, C# will anyway be the mainstream language on .NET. But this does not depreciate F# - it's simply useful to know and understand it.

4. It's simply wrong to set OOP oppositely to FP, so don't worry much about usability of your existing knowledge. Contemporary FP languages supports all you know in OOP (F# and Haskell are not exceptions here). But they push you to use a bit different way of thinking about programs you write. FP is opposite to imperative programming (which really becomes less and less interesting), and OOP is a kind of extension to both of these techniques allowing you to describe real world entities more conveniently. That's the second "truth in the middle" I was talking about.

The most interesting part - the links:

Free lunch is over. That's for those ones who isn't fully convinced programming techniques we use now are going to change. Not in future - they're changing right now.

So you're curious, what is a monad? Here is a very short answer @ stackoverflow explaining this. At least this will give you a kind of short introduction. To fully understand why they have appeared in FP and bind the description with practice, take a look at the description of IO in Haskell.

Let's bind this to your existing C# knowledge now. The Marvels of Monads and Functional .NET - LINQ or Language Integrated Monads? - good articles to start from. You'll learn that IEnumerable<T> is just a particular example of monad, but LINQ syntax in C# allows you to deal with generally any others of them.
    I suspect now you are either even more interested, curious and eager to learn, or have already decided to leave this for some future studying (btw, that's pretty reasonable as well). So if you're facing #1 case, here are great C9 videos explaining this:
    Finally, you should take a look at the following categories at Channel 9:
    Have a nice trip ;)

    P.S. If you work @ Xtensive, the videos can be found at "\\Storage\Media\Video\IT\Functional Programming".

    October 22, 2009

    Shame to me: DO4 installer does not properly sets up everything for IIS 7 and 64-bit Windows

    That's true I discovered few days ago after post in our forum. The story behind is quite simple: I still use Windows Server 2003 (it's still alive ;) ) at my workplace and Windows Vista at home. Since I'm the person responsible for installer, I launch it and test mainly by my own, although I frequently ask the guys from my team to test it as well on their own PCs. So how this could happen that:
    • Nothing is properly installed on any 64 bit Windows. Basically, everything is copied, but nothing more!
    • The process of ASP.NET sample installation was actually changing nothing at all on IIS 7!
    64 bit Windows support was broken mainly because of wrong detection of Visual Studio paths there and few an issue related to bracket handling in .bat files - I'll write about this later, but briefly, even echo "Something (x86)" may lead to an error there. Much worse case is if "%somePathWithBrackets%"=="" ... :)

    ASP.NET sample installation was broken by much simpler reason: I used the same VBScript as for IIS 6 there instead of appcmd.exe. Shame for me :( Btw, I suspect I know why I didn't knew this: I have installed DO4 on home PC. Since installer is actually incapable of not just installing the sample, but uninstalling it as well, I was simply opening my own pre-configured version of it. "Voila - it works!"

    The only thing surprising me is why no one wrote about all this stuff. We track all the downloads (likely, you know this - we send few notifications after each one). So I know how many persons have downloaded DO4 e.g. in last week, and I got just one complain...

    Btw, we know documentation and installation are the worst problems people face - by our "exit polls". I admit I did may be 90% of what I could to improve the installation, integration with MSBuild and very basic usability during last month, so I hope these bugs are the last important ones there - today I'm planning to fix the last minor problem with ASP.NET sample that currently exposes itself on 64 bit Windows Server 2008 (probably, the issue is related to any IIS 7). The nightly build that is published right now installs everything properly, the only exception is this issue.

    And finally, a single conclusion: I should always keep in mind which failures are the most expensive!

    Earlier I wrote our testing process is very comprehensive: we're constantly keeping just few failing tests (mainly, for issues we're resolving right now) and pretty good code coverage. Take a look at TeamCity screenshots I made few minutes ago.

      

    Imagine:

    • 36 build configurations running tests on all versions of all databases we support
    • ~ 1100 tests for Xtensive.Storage 
    • ~ 1000 tests for its dependencies
    • Pre-commit tests, 5 build agents and so on... 
    • But these test do not covers installer!

    Yes, testing of installers is what really hard to automatize. But does good product testing matters at all, if its installer fails in may be 80% of cases because of just few bugs? I'm thinking, how costly may such a buggy installer be... "I have 64 bit Windows. Install was ok, but nothing works. So I uninstalled it and added the product and the company behind into in my personal blacklist." Likely, that's what really happened with 70% of our downloads in just first 5 minutes of their completion. Taking into account there is poor documentation as well, I can easily raise up the number to 95%. So that's the answer to my own question: "Why no one reports there are bugs in installer?".

    I apologize for this huge mistake I made. I hope if you're reading this blog, you're the one of persons that did not blacklisted us because of this :( We'll publish completely polished installer in a day or so.

    October 21, 2009

    Great tool: yUML

    I was always curious how Dmitri Maximov creates his nice diagrams, but as it frequently happens, I asked this just today. His answer has astonished me: he uses yUML, online UML diagram generator. So actually his diagrams are defined inside <img src="..." /> tags!

    That's really nice!

    October 20, 2009

    100% score on LINQ tests at ORMBattle.NET!

    Today's nightly build of DO4 gets 100% score on LINQ tests at ORMBattle.NET! So currently LINQ scoreboard looks like:
    • 100,0%: DataObjects.Net
    • 88,9%: LINQ to SQL
    • 75,2%: ADO.NET Entity Framework
    • 35,9%: BLToolkit
    • 29,9%: NHibernate
    I listed the results for freeware and Microsoft products only. We plan to update ORMBattle.NET scoreboard this or next week.

    The official launch of Visual Studio 2010 and .NET Framework 4 is March 22nd, 2010.

    That's a bit disappointing - not sure, why, but I hoped to see it this year... On the other hand, having a "go live" release (Beta 2) right now is really cool.

    So what do you think, should we start porting DO4 to .NET 4.0 \ VS 2010 Beta2 right now (actually we thought about this), or it's better to stick to VS.NET 2008 and .NET 3.5 till the new year?

    Recommended article: The Rx Framework (LINQ to events)

    Check out:
    This is really interesting and impressive! It seems Erik Meijer and the team at Microsoft Research have shown up the next great creation after LINQ.

    Upcoming DisconnectedState: preliminary example code

    If you don't know about DisconnectedState, please read first section of this post first.

    Here is what we're going to deliver:
    var keys = new List<Key>();
    DisconnectedState state;
    
    using (var session = Session.OpenDisconnected(domain)) {
      // Validation is off here by default,
      // so next line passes even if it violates validation rules
      var p = new Person { Name = "Alex", Age = -1 } 
    
      // DisconnectedState manages its own local transactions
      // and implements unit of work pattern.
      try {
        () => {
          p.Age = 1;
          throw new Exception();
        }.InvokeTransactionally(); // If you don't know, 
        // that's the way to create and invoke anonymous
        // transactional method. 
        // See TransactionalExceptions type.
      }
      catch {}
      Assert.AreEqual(-1, p.Age); // Passes.
    
      var prefetchingQuery = 
        Query<Customer>.All.Where(...)
          .Prefetch(c => c.Orders);
    
      // This code will run in single local and single
      // database transaction. All the fetched instances
      // will be cached in DisconnectedState.
      for (customer c in Query<Customer>.All) {
        c.Name = string.Format("{0} ({1})", // Local change
          c.Name, c.Orders.Count);
        keys.Add(c.Key);
      }
    
      // Nothing is changed in the database yet -
      // all the changes are cached only in DisconnectedState.
    
      // Let's detach the state
      state = session.DisconnectedState;
    }
    
    
    // And attach it to a new Session
    using (var session = Session.OpenDisconnected(domain, state)) {
      // This loop won't hit the database,
      // since everything it needs is already there
      for (customer c in keys.Select(Query<Customer>.Single(key)) {
        Console.WriteLine("Customer: {0}", c.Name);
        for (order o in c.Orders)
          Console.WriteLine("  Order: {0}", o.ToString());
      }
    
      // Let's accept changes now.
      // They'll be persisted in a single transaction;
      // version checks and validation will be performed
      // for all the changed objects.
      state.AcceptChanges();
    }

    It will be possible to extract the changes and apply them separately:
    var disconnectedState = ...
    var session = ...
    // Let's create a change set containing both changes
    // and original versions of all the affected objects. 
    var changeSet = disconnectedState.GetChangeSet();
    // Here we clone it by serialization,
    // see Xtensive.Core.Helpers.Cloner
    var changeSetClone = Cloner.Default.Clone(changeSet); 
    // And finally, we apply the changes to
    // another Session.
    changeSetClone.Apply(session);

    Initially DisconnectedState will track:
    • Entity creation, change and removal
    • Similar EntitySet operations.
    So when you call ApplyChanges method (btw, it's possible to pass another Session there), change set is actually applied as sequence of Entity and EntitySet method invocations on objects from this Session. In future it will be possible to replicate top-level SessionBound method calls instead of low-level changed, that must bring true action sequence replication there.

    It will be possible to:
    • RejectChanges. This won't affect any fetched information.
    • Prevent any transparent fetches. By default they're on - i.e. such a Session works in semi-connected mode, but you can restrict them to be available in special code blocks only (I'm not fully sure about final syntax of this, my current best option is () => { ... }.InvokeConnected() ).
    • Serialize DisconnectedState and deserialize it. You won't need to create Session and Domain objects on the client to make this working (in this case we will provide "default" domain resembling the structure of original Domain object; Session there will also be "closed" - it won't allow to transparently fetch anything there).
    You'll see this feature working in a week or two. I'd be glad to hear any comments.

    October 18, 2009

    Issues in v4.1 RC


    Unfortunately, we identified a set of serious issues in DataObjects.Net v4.1 RC:
    • CopyIndirectDependencies.targets packed there contains a bug leading to an error on building "Rebuild" target, if there are any project dependencies in the project it is imported from (i.e. there are references to projects, but not just to .dlls). So "Rebuild" in VS.NET does not work in this case. Imagine, we've been using this .targets file for about a month, and noticed this just on this week - it seems none of us uses Rebuid, since Build works perfectly with our projects ;) Alexey Kofman said he faced an issue with Rebuild before, but being in hurry, we didn't pay appropriate attention to this.
    • Our new DataObjects.Net.targets fail if any version of PostSharp is installed on your machine. I mentioned it should be uninstalled, but we understood that people does not pay attention to this. Moreover, it's quite bad if DO4 conflicts with installed PostSharp, especially taking into account the fact that a bit earlier we require you to install it.
    • Since now we copy all .pdb files into bin folder, you may get a set of breaks on exceptions thrown and caught inside Xtensive.Core during Domain shartup. This is normal - we never use exceptions in normal operation flow, but in some cases .NET does not leave us a choice. E.g. when AssociateProvider tries to instantiate candidate generics, it's impossible to make .NET to notify us this is not possible without an exception (e.g. if "where T: ..." condition fails for a particular generic). So we catch it and try the next one in sequence - that's what AssociateProvider implies. Implementing all these checks manually is pretty complex, so we don't do this for now. But it's not a good idea to copy .pdb and make VS.NET to show all of them as well.
    • A debug code writing assemblies with built Tuple types is not commented out in Xtensive.Core project sources. So Xtensive.Core tries to create .dll files in bin folder, and if the process had no write permission there, it fails.
    Fortunately, there were no any serious issues related to runtime behavior - continuous integration and testing we run almost immediately exposes them. As you see, the issues I just mentioned are related to build and installation process, which isn't covered by our test suite. That's why we got them...

    Ok, now the good news:
    • All these issues are already resolved
    • New v4.1 installer will be published today.
    I'm really sorry for this incident. We'll identify all the persons that downloaded v4.1 RC and notify them personally - likely, this build was a real disappointment for many of them.

    October 16, 2009

    DataObjects.Net v4.1 RC is updated

    I just updated v4.1 installer. A bug related to output in Console sample is fixed there; new integrated logger is there as well.

    October 15, 2009

    DataObjects.Net v4.1: easy logging and uber-batching in action

    Initially I planned to talk about future queries here, but since almost all space here takes produced log, I decided to mention it as well.

    Note: If you're viewing this post in RSS/Atom reader, open its origin to see it with syntax highlighting.

    Let's start right from code. If you're interested in conclusions, go to the tail of this post.

    Program.cs:
    using System;
    using System.Linq;
    using Xtensive.Storage;
    using Xtensive.Storage.Configuration;
    using Batching.Model;
    
    namespace Batching.Model
    {
      [HierarchyRoot]
      public class MyEntity : Entity
      {
        [Field, Key]
        public int Id { get; private set; }
    
        [Field(Length = 100)]
        public string Text { get; set; }
      }
    }
    
    namespace Batching
    {
      class Program
      {
        static void Main(string[] args)
        {
          var config = DomainConfiguration.Load("Default");
          var domain = Domain.Build(config);
    
          using (Session.Open(domain)) {
            using (var transactionScope = Transaction.Open()) {
              var entity1 = new MyEntity {
                Text = "Entity 1"
              }; // Nothing is sent to server yet
              
              var entity2 = new MyEntity {
                Text = "Entity 2"
              }; // Nothing is sent to server yet
    
              var futureCount = Query.ExecuteFutureScalar(
                () => Query<MyEntity>.All.Count());
    
              foreach (var e in Query<MyEntity>.All) // Batch is sent
                Console.WriteLine("Entity.Text: {0}", e.Text); 
              Console.WriteLine("Count: {0}", futureCount.Value);
    
              // Committing transaction
              transactionScope.Complete();
            }
          }
          Console.ReadLine();
        }
      }
    }
    
    App.config:
    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
      <configSections>
        <section name="Xtensive.Storage" 
                 type="Xtensive.Storage.Configuration.Elements.ConfigurationSection, Xtensive.Storage" />
        <section name="Xtensive.Core.Diagnostics" 
                 type="Xtensive.Core.Diagnostics.Configuration.ConfigurationSection, Xtensive.Core" />
      </configSections>
      <Xtensive.Core.Diagnostics>
        <logs>
          <log name=""        provider="Null" />
          <log name="Storage" provider="File" formatString="{5}"/>
        </logs>
      </Xtensive.Core.Diagnostics>
      <Xtensive.Storage>
        <domains>
          <domain name="Default" upgradeMode="Recreate" connectionUrl="sqlserver://localhost/DO40-Tests" >
            <types>
              <add assembly="Batching" />
            </types>
          </domain>
        </domains>
      </Xtensive.Storage>
    </configuration>
    
    Output:
    Entity.Text: Entity 1
    Entity.Text: Entity 2
    Count: 2
    
    And finally, its log - Batching.exe.log:
    Building Domain: started.
    Creating Domain: started.
    Creating Domain: completed.
    Creating HandlerFactory: started.
    Creating HandlerFactory: completed.
    Creating DomainHandler: started.
    Creating DomainHandler: completed.
    Creating NameBuilder: started.
    Creating NameBuilder: completed.
    Creating DomainHandler: started.
    Creating DomainHandler: completed.
    Building Model: started.
    Building Model Definition: started.
    Defining 'Types': started.
    Defining 'Xtensive.Storage.IEntity': started.
    Defining 'Xtensive.Storage.IEntity': completed.
    Defining 'Xtensive.Storage.Entity': started.
    Field: 'TypeId'
    Defining 'Xtensive.Storage.Entity': completed.
    Defining 'Xtensive.Storage.Metadata.MetadataBase': started.
    Defining 'Xtensive.Storage.Metadata.MetadataBase': completed.
    Defining 'Xtensive.Storage.Metadata.Type': started.
    Field: 'Id'
    Field: 'Name'
    Field: 'TypeId'
    Index: 'IX_Name'
    Hierarchy: 'Type'
    Defining 'Xtensive.Storage.Metadata.Type': completed.
    Defining 'Xtensive.Storage.Metadata.Extension': started.
    Field: 'Name'
    Field: 'Text'
    Field: 'Data'
    Field: 'TypeId'
    Hierarchy: 'Extension'
    Defining 'Xtensive.Storage.Metadata.Extension': completed.
    Defining 'Xtensive.Storage.Structure': started.
    Defining 'Xtensive.Storage.Structure': completed.
    Defining 'Xtensive.Storage.Metadata.Assembly': started.
    Field: 'Name'
    Field: 'Version'
    Field: 'TypeId'
    Hierarchy: 'Assembly'
    Defining 'Xtensive.Storage.Metadata.Assembly': completed.
    Defining 'Batching.Model.MyEntity': started.
    Field: 'Id'
    Field: 'Text'
    Field: 'TypeId'
    Hierarchy: 'MyEntity'
    Defining 'Batching.Model.MyEntity': completed.
    Defining 'Types': completed.
    Building Model Definition: completed.
    Building Custom Definitions: started.
    Building Custom Definitions: completed.
    Inspecting model definition: started.
    Inspecting hierarchy 'Type'
    Inspecting hierarchy 'Extension'
    Inspecting hierarchy 'Assembly'
    Inspecting hierarchy 'MyEntity'
    Inspecting type 'Entity'
    Inspecting type 'MetadataBase'
    Inspecting type 'Type'
    Inspecting type 'Extension'
    Inspecting type 'Structure'
    Inspecting type 'Assembly'
    Inspecting type 'MyEntity'
    Inspecting model definition: completed.
    Processing fixup actions: started.
    Executing action: 'Mark 'Type.TypeId' field as system.'
    Executing action: 'Reorder fields in 'Type' type.'
    Executing action: 'Add primary index to 'Type''
    Executing action: 'Mark 'Extension.TypeId' field as system.'
    Executing action: 'Reorder fields in 'Extension' type.'
    Executing action: 'Mark 'Extension.Name' field as not nullable.'
    Executing action: 'Add primary index to 'Extension''
    Executing action: 'Mark 'Assembly.TypeId' field as system.'
    Executing action: 'Reorder fields in 'Assembly' type.'
    Executing action: 'Mark 'Assembly.Name' field as not nullable.'
    Executing action: 'Add primary index to 'Assembly''
    Executing action: 'Mark 'MyEntity.TypeId' field as system.'
    Executing action: 'Reorder fields in 'MyEntity' type.'
    Executing action: 'Add primary index to 'MyEntity''
    Executing action: 'Remove 'Entity' type.'
    Executing action: 'Remove 'MetadataBase' type.'
    Processing fixup actions: completed.
    Building Actual Model: started.
    Building Types: started.
    Building Type: started.
    Building declared field 'Type.Id'
    Building Extension: started.
    Building declared field 'Extension.Name'
    Building Assembly: started.
    Building declared field 'Assembly.Name'
    Building MyEntity: started.
    Building declared field 'MyEntity.Id'
    Building Types: completed.
    Building Fields: started.
    Building declared field 'Type.TypeId'
    Building declared field 'Type.Name'
    Building declared field 'Extension.TypeId'
    Building declared field 'Extension.Text'
    Building declared field 'Extension.Data'
    Building declared field 'Assembly.TypeId'
    Building declared field 'Assembly.Version'
    Building declared field 'MyEntity.TypeId'
    Building declared field 'MyEntity.Text'
    Building Fields: completed.
    Building Associations: started.
    Building Associations: completed.
    Building Indexes: started.
    Building index 'IX_Name'
    Building index 'PK_Type'
    Building index 'PK_Extension'
    Building index 'PK_Assembly'
    Building index 'PK_MyEntity'
    Building Indexes: completed.
    Building Actual Model: completed.
    Building Model: completed.
    Opening session 'Name = System, Options = Default, CacheType = LruWeak, CacheSize = 16384, DefaultIsolationLevel = ReadCommitted'.
    Session 'System, #1'. Creating connection 'sqlserver://localhost/DO40-Tests'.
    Session 'System, #1'. Opening connection 'sqlserver://localhost/DO40-Tests'.
    Session 'System, #1'. Beginning transaction @ ReadCommitted.
    Synchronizing schema in Recreate mode: started.
    Target schema:
    Extracted schema:
    Clearing comparison result:
    Status: TargetIsSubset, Can't upgrade safely
    Hints:
      
    Difference:
      "./" != "./" (NodeDifference): 0
        +Tables: "./Tables (4)" != "./Tables (0)" (NodeCollectionDifference): 4 change(s)
          "./Tables/Metadata.Type" != "" (NodeDifference): Removed
            +Columns: "./Tables/Metadata.Type/Columns (3)" != "" (NodeCollectionDifference): 3 change(s)
              "./Tables/Metadata.Type/Columns/Id" != "" (NodeDifference): Removed
                +Type: "Type: Int32, Length: null" != "" (ValueDifference): values differ
              "./Tables/Metadata.Type/Columns/TypeId" != "" (NodeDifference): Removed
                +Type: "Type: Int32, Length: null" != "" (ValueDifference): values differ
              "./Tables/Metadata.Type/Columns/Name" != "" (NodeDifference): Removed
                +Type: "Type: String?, Length: 1000" != "" (ValueDifference): values differ
            +PrimaryIndex: "./Tables/Metadata.Type/PrimaryIndex (PK_Type)" != "" (NodeDifference): Removed
              +IsUnique: "True" != "False" (ValueDifference): values differ
              +KeyColumns: "./Tables/Metadata.Type/PrimaryIndex/KeyColumns (1)" != "" (NodeCollectionDifference): 1 change(s)
                "./Tables/Metadata.Type/PrimaryIndex/KeyColumns/0" != "" (NodeDifference): Removed
                  +Direction: "Positive" != "None" (ValueDifference): values differ
                  +Value: "./Tables/Metadata.Type/Columns/Id" != "" (ValueDifference): values differ
            +SecondaryIndexes: "./Tables/Metadata.Type/SecondaryIndexes (1)" != "" (NodeCollectionDifference): 1 change(s)
              "./Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name" != "" (NodeDifference): Removed
                +IsUnique: "True" != "False" (ValueDifference): values differ
                +KeyColumns: "./Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name/KeyColumns (1)" != "" (NodeCollectionDifference): 1 change(s)
                  "./Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name/KeyColumns/0" != "" (NodeDifference): Removed
                    +Direction: "Positive" != "None" (ValueDifference): values differ
                    +Value: "./Tables/Metadata.Type/Columns/Name" != "" (ValueDifference): values differ
                +PrimaryKeyColumns: "./Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name/PrimaryKeyColumns (1)" != "" (NodeCollectionDifference): 1 change(s)
                  "./Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name/PrimaryKeyColumns/0" != "" (NodeDifference): Removed
                    +Direction: "Positive" != "None" (ValueDifference): values differ
                    +Value: "./Tables/Metadata.Type/Columns/Id" != "" (ValueDifference): values differ
                +IncludedColumns: "./Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name/ValueColumns (1)" != "" (NodeCollectionDifference): 1 change(s)
                  "./Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name/IncludedColumns/0" != "" (NodeDifference): Removed
                    +Value: "./Tables/Metadata.Type/Columns/TypeId" != "" (ValueDifference): values differ
          "./Tables/Metadata.Extension" != "" (NodeDifference): Removed
            +Columns: "./Tables/Metadata.Extension/Columns (4)" != "" (NodeCollectionDifference): 4 change(s)
              "./Tables/Metadata.Extension/Columns/Name" != "" (NodeDifference): Removed
                +Type: "Type: String, Length: 1024" != "" (ValueDifference): values differ
              "./Tables/Metadata.Extension/Columns/TypeId" != "" (NodeDifference): Removed
                +Type: "Type: Int32, Length: null" != "" (ValueDifference): values differ
              "./Tables/Metadata.Extension/Columns/Text" != "" (NodeDifference): Removed
                +Type: "Type: String?, Length: null" != "" (ValueDifference): values differ
              "./Tables/Metadata.Extension/Columns/Data" != "" (NodeDifference): Removed
                +Type: "Type: Byte[]?, Length: null" != "" (ValueDifference): values differ
            +PrimaryIndex: "./Tables/Metadata.Extension/PrimaryIndex (PK_Extension)" != "" (NodeDifference): Removed
              +IsUnique: "True" != "False" (ValueDifference): values differ
              +KeyColumns: "./Tables/Metadata.Extension/PrimaryIndex/KeyColumns (1)" != "" (NodeCollectionDifference): 1 change(s)
                "./Tables/Metadata.Extension/PrimaryIndex/KeyColumns/0" != "" (NodeDifference): Removed
                  +Direction: "Positive" != "None" (ValueDifference): values differ
                  +Value: "./Tables/Metadata.Extension/Columns/Name" != "" (ValueDifference): values differ
          "./Tables/Metadata.Assembly" != "" (NodeDifference): Removed
            +Columns: "./Tables/Metadata.Assembly/Columns (3)" != "" (NodeCollectionDifference): 3 change(s)
              "./Tables/Metadata.Assembly/Columns/Name" != "" (NodeDifference): Removed
                +Type: "Type: String, Length: 1024" != "" (ValueDifference): values differ
              "./Tables/Metadata.Assembly/Columns/TypeId" != "" (NodeDifference): Removed
                +Type: "Type: Int32, Length: null" != "" (ValueDifference): values differ
              "./Tables/Metadata.Assembly/Columns/Version" != "" (NodeDifference): Removed
                +Type: "Type: String?, Length: 64" != "" (ValueDifference): values differ
            +PrimaryIndex: "./Tables/Metadata.Assembly/PrimaryIndex (PK_Assembly)" != "" (NodeDifference): Removed
              +IsUnique: "True" != "False" (ValueDifference): values differ
              +KeyColumns: "./Tables/Metadata.Assembly/PrimaryIndex/KeyColumns (1)" != "" (NodeCollectionDifference): 1 change(s)
                "./Tables/Metadata.Assembly/PrimaryIndex/KeyColumns/0" != "" (NodeDifference): Removed
                  +Direction: "Positive" != "None" (ValueDifference): values differ
                  +Value: "./Tables/Metadata.Assembly/Columns/Name" != "" (ValueDifference): values differ
          "./Tables/MyEntity" != "" (NodeDifference): Removed
            +Columns: "./Tables/MyEntity/Columns (3)" != "" (NodeCollectionDifference): 3 change(s)
              "./Tables/MyEntity/Columns/Id" != "" (NodeDifference): Removed
                +Type: "Type: Int32, Length: null" != "" (ValueDifference): values differ
              "./Tables/MyEntity/Columns/TypeId" != "" (NodeDifference): Removed
                +Type: "Type: Int32, Length: null" != "" (ValueDifference): values differ
              "./Tables/MyEntity/Columns/Text" != "" (NodeDifference): Removed
                +Type: "Type: String?, Length: 100" != "" (ValueDifference): values differ
            +PrimaryIndex: "./Tables/MyEntity/PrimaryIndex (PK_MyEntity)" != "" (NodeDifference): Removed
              +IsUnique: "True" != "False" (ValueDifference): values differ
              +KeyColumns: "./Tables/MyEntity/PrimaryIndex/KeyColumns (1)" != "" (NodeCollectionDifference): 1 change(s)
                "./Tables/MyEntity/PrimaryIndex/KeyColumns/0" != "" (NodeDifference): Removed
                  +Direction: "Positive" != "None" (ValueDifference): values differ
                  +Value: "./Tables/MyEntity/Columns/Id" != "" (ValueDifference): values differ
        +Sequences: "./Sequences (1)" != "./Sequences (0)" (NodeCollectionDifference): 1 change(s)
          "./Sequences/Int32-Generator" != "" (NodeDifference): Removed
            +Increment: "128" != "0" (ValueDifference): values differ
    Actions:
      GroupingNode, Comment=Prepare
        GroupingNode, Comment=.
          GroupingNode, Comment=Sequences[]
            GroupingNode, Comment=Int32-Generator
              RemoveNode, Path=Sequences/Int32-Generator
          GroupingNode, Comment=Tables[]
            GroupingNode, Comment=Metadata.Type
              RemoveNode, Path=Tables/Metadata.Type
            GroupingNode, Comment=Metadata.Extension
              RemoveNode, Path=Tables/Metadata.Extension
            GroupingNode, Comment=Metadata.Assembly
              RemoveNode, Path=Tables/Metadata.Assembly
            GroupingNode, Comment=MyEntity
              RemoveNode, Path=Tables/MyEntity
      GroupingNode, Comment=TemporaryRename
      
    Session 'System, #1'. Schema upgrade script:
    DROP TABLE [dbo].[Int32-Generator];
    DROP TABLE [dbo].[Metadata.Type];
    DROP TABLE [dbo].[Metadata.Extension];
    DROP TABLE [dbo].[Metadata.Assembly];
    DROP TABLE [dbo].[MyEntity];
    Session 'n/a'. SQL batch: 
    DROP TABLE [dbo].[Int32-Generator];
    DROP TABLE [dbo].[Metadata.Type];
    DROP TABLE [dbo].[Metadata.Extension];
    DROP TABLE [dbo].[Metadata.Assembly];
    DROP TABLE [dbo].[MyEntity];
    Comparison result:
    Status: TargetIsSuperset, Can upgrade safely
    Hints:
      
    Difference:
      "./" != "./" (NodeDifference): 0
        +Tables: "./Tables (0)" != "./Tables (4)" (NodeCollectionDifference): 4 change(s)
          "" != "./Tables/Metadata.Type" (NodeDifference): Created
            +Columns: "" != "./Tables/Metadata.Type/Columns (3)" (NodeCollectionDifference): 3 change(s)
              "" != "./Tables/Metadata.Type/Columns/Id" (NodeDifference): Created
                +Type: "" != "Type: Int32, Length: null" (ValueDifference): values differ
              "" != "./Tables/Metadata.Type/Columns/TypeId" (NodeDifference): Created
                +Type: "" != "Type: Int32, Length: null" (ValueDifference): values differ
              "" != "./Tables/Metadata.Type/Columns/Name" (NodeDifference): Created
                +Type: "" != "Type: String?, Length: 1000" (ValueDifference): values differ
            +PrimaryIndex: "" != "./Tables/Metadata.Type/PrimaryIndex (PK_Type)" (NodeDifference): Created
              +IsUnique: "False" != "True" (ValueDifference): values differ
              +KeyColumns: "" != "./Tables/Metadata.Type/PrimaryIndex/KeyColumns (1)" (NodeCollectionDifference): 1 change(s)
                "" != "./Tables/Metadata.Type/PrimaryIndex/KeyColumns/0" (NodeDifference): Created
                  +Direction: "None" != "Positive" (ValueDifference): values differ
                  +Value: "" != "./Tables/Metadata.Type/Columns/Id" (ValueDifference): values differ
            +SecondaryIndexes: "" != "./Tables/Metadata.Type/SecondaryIndexes (1)" (NodeCollectionDifference): 1 change(s)
              "" != "./Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name" (NodeDifference): Created
                +IsUnique: "False" != "True" (ValueDifference): values differ
                +KeyColumns: "" != "./Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name/KeyColumns (1)" (NodeCollectionDifference): 1 change(s)
                  "" != "./Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name/KeyColumns/0" (NodeDifference): Created
                    +Direction: "None" != "Positive" (ValueDifference): values differ
                    +Value: "" != "./Tables/Metadata.Type/Columns/Name" (ValueDifference): values differ
                +PrimaryKeyColumns: "" != "./Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name/PrimaryKeyColumns (1)" (NodeCollectionDifference): 1 change(s)
                  "" != "./Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name/PrimaryKeyColumns/0" (NodeDifference): Created
                    +Direction: "None" != "Positive" (ValueDifference): values differ
                    +Value: "" != "./Tables/Metadata.Type/Columns/Id" (ValueDifference): values differ
                +IncludedColumns: "" != "./Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name/ValueColumns (1)" (NodeCollectionDifference): 1 change(s)
                  "" != "./Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name/IncludedColumns/0" (NodeDifference): Created
                    +Value: "" != "./Tables/Metadata.Type/Columns/TypeId" (ValueDifference): values differ
          "" != "./Tables/Metadata.Extension" (NodeDifference): Created
            +Columns: "" != "./Tables/Metadata.Extension/Columns (4)" (NodeCollectionDifference): 4 change(s)
              "" != "./Tables/Metadata.Extension/Columns/Name" (NodeDifference): Created
                +Type: "" != "Type: String, Length: 1024" (ValueDifference): values differ
              "" != "./Tables/Metadata.Extension/Columns/TypeId" (NodeDifference): Created
                +Type: "" != "Type: Int32, Length: null" (ValueDifference): values differ
              "" != "./Tables/Metadata.Extension/Columns/Text" (NodeDifference): Created
                +Type: "" != "Type: String?, Length: null" (ValueDifference): values differ
              "" != "./Tables/Metadata.Extension/Columns/Data" (NodeDifference): Created
                +Type: "" != "Type: Byte[]?, Length: null" (ValueDifference): values differ
            +PrimaryIndex: "" != "./Tables/Metadata.Extension/PrimaryIndex (PK_Extension)" (NodeDifference): Created
              +IsUnique: "False" != "True" (ValueDifference): values differ
              +KeyColumns: "" != "./Tables/Metadata.Extension/PrimaryIndex/KeyColumns (1)" (NodeCollectionDifference): 1 change(s)
                "" != "./Tables/Metadata.Extension/PrimaryIndex/KeyColumns/0" (NodeDifference): Created
                  +Direction: "None" != "Positive" (ValueDifference): values differ
                  +Value: "" != "./Tables/Metadata.Extension/Columns/Name" (ValueDifference): values differ
          "" != "./Tables/Metadata.Assembly" (NodeDifference): Created
            +Columns: "" != "./Tables/Metadata.Assembly/Columns (3)" (NodeCollectionDifference): 3 change(s)
              "" != "./Tables/Metadata.Assembly/Columns/Name" (NodeDifference): Created
                +Type: "" != "Type: String, Length: 1024" (ValueDifference): values differ
              "" != "./Tables/Metadata.Assembly/Columns/TypeId" (NodeDifference): Created
                +Type: "" != "Type: Int32, Length: null" (ValueDifference): values differ
              "" != "./Tables/Metadata.Assembly/Columns/Version" (NodeDifference): Created
                +Type: "" != "Type: String?, Length: 64" (ValueDifference): values differ
            +PrimaryIndex: "" != "./Tables/Metadata.Assembly/PrimaryIndex (PK_Assembly)" (NodeDifference): Created
              +IsUnique: "False" != "True" (ValueDifference): values differ
              +KeyColumns: "" != "./Tables/Metadata.Assembly/PrimaryIndex/KeyColumns (1)" (NodeCollectionDifference): 1 change(s)
                "" != "./Tables/Metadata.Assembly/PrimaryIndex/KeyColumns/0" (NodeDifference): Created
                  +Direction: "None" != "Positive" (ValueDifference): values differ
                  +Value: "" != "./Tables/Metadata.Assembly/Columns/Name" (ValueDifference): values differ
          "" != "./Tables/MyEntity" (NodeDifference): Created
            +Columns: "" != "./Tables/MyEntity/Columns (3)" (NodeCollectionDifference): 3 change(s)
              "" != "./Tables/MyEntity/Columns/Id" (NodeDifference): Created
                +Type: "" != "Type: Int32, Length: null" (ValueDifference): values differ
              "" != "./Tables/MyEntity/Columns/TypeId" (NodeDifference): Created
                +Type: "" != "Type: Int32, Length: null" (ValueDifference): values differ
              "" != "./Tables/MyEntity/Columns/Text" (NodeDifference): Created
                +Type: "" != "Type: String?, Length: 100" (ValueDifference): values differ
            +PrimaryIndex: "" != "./Tables/MyEntity/PrimaryIndex (PK_MyEntity)" (NodeDifference): Created
              +IsUnique: "False" != "True" (ValueDifference): values differ
              +KeyColumns: "" != "./Tables/MyEntity/PrimaryIndex/KeyColumns (1)" (NodeCollectionDifference): 1 change(s)
                "" != "./Tables/MyEntity/PrimaryIndex/KeyColumns/0" (NodeDifference): Created
                  +Direction: "None" != "Positive" (ValueDifference): values differ
                  +Value: "" != "./Tables/MyEntity/Columns/Id" (ValueDifference): values differ
        +Sequences: "./Sequences (0)" != "./Sequences (1)" (NodeCollectionDifference): 1 change(s)
          "" != "./Sequences/Int32-Generator" (NodeDifference): Created
            +Increment: "0" != "128" (ValueDifference): values differ
    Actions:
      GroupingNode, Comment=Prepare
      GroupingNode, Comment=TemporaryRename
      GroupingNode, Comment=Upgrade
        GroupingNode, Comment=.
          GroupingNode, Comment=Tables[]
            GroupingNode, Comment=Metadata.Type
              CreateNode, Path=, Type=TableInfo, Name=Metadata.Type, Index=0
              GroupingNode, Comment=Columns[]
                GroupingNode, Comment=Id
                  CreateNode, Path=Tables/Metadata.Type, Type=ColumnInfo, Name=Id, Index=0
                  PropertyChange, Path=Tables/Metadata.Type/Columns/Id, Type=Type: Int32, Length: null
                GroupingNode, Comment=TypeId
                  CreateNode, Path=Tables/Metadata.Type, Type=ColumnInfo, Name=TypeId, Index=1
                  PropertyChange, Path=Tables/Metadata.Type/Columns/TypeId, Type=Type: Int32, Length: null
                GroupingNode, Comment=Name
                  CreateNode, Path=Tables/Metadata.Type, Type=ColumnInfo, Name=Name, Index=2
                  PropertyChange, Path=Tables/Metadata.Type/Columns/Name, Type=Type: String?, Length: 1000
              GroupingNode, Comment=PK_Type
                CreateNode, Path=Tables/Metadata.Type, Type=PrimaryIndexInfo, Name=PK_Type
                PropertyChange, Path=Tables/Metadata.Type/PrimaryIndex, IsUnique=True
                GroupingNode, Comment=KeyColumns[]
                  GroupingNode, Comment=0
                    CreateNode, Path=Tables/Metadata.Type/PrimaryIndex, Type=KeyColumnRef, Name=0, Index=0
                    PropertyChange, Path=Tables/Metadata.Type/PrimaryIndex/KeyColumns/0, Value=Tables/Metadata.Type/Columns/Id, Direction=Positive
              GroupingNode, Comment=SecondaryIndexes[]
                GroupingNode, Comment=Type.IX_Name
                  CreateNode, Path=Tables/Metadata.Type, Type=SecondaryIndexInfo, Name=Type.IX_Name, Index=0
                  PropertyChange, Path=Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name, IsUnique=True
                  GroupingNode, Comment=KeyColumns[]
                    GroupingNode, Comment=0
                      CreateNode, Path=Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name, Type=KeyColumnRef, Name=0, Index=0
                      PropertyChange, Path=Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name/KeyColumns/0, Value=Tables/Metadata.Type/Columns/Name, Direction=Positive
                  GroupingNode, Comment=PrimaryKeyColumns[]
                    GroupingNode, Comment=0
                      CreateNode, Path=Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name, Type=PrimaryKeyColumnRef, Name=0, Index=0
                      PropertyChange, Path=Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name/PrimaryKeyColumns/0, Value=Tables/Metadata.Type/Columns/Id, Direction=Positive
                  GroupingNode, Comment=IncludedColumns[]
                    GroupingNode, Comment=0
                      CreateNode, Path=Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name, Type=IncludedColumnRef, Name=0, Index=0
                      PropertyChange, Path=Tables/Metadata.Type/SecondaryIndexes/Type.IX_Name/IncludedColumns/0, Value=Tables/Metadata.Type/Columns/TypeId
            GroupingNode, Comment=Metadata.Extension
              CreateNode, Path=, Type=TableInfo, Name=Metadata.Extension, Index=1
              GroupingNode, Comment=Columns[]
                GroupingNode, Comment=Name
                  CreateNode, Path=Tables/Metadata.Extension, Type=ColumnInfo, Name=Name, Index=0
                  PropertyChange, Path=Tables/Metadata.Extension/Columns/Name, Type=Type: String, Length: 1024
                GroupingNode, Comment=TypeId
                  CreateNode, Path=Tables/Metadata.Extension, Type=ColumnInfo, Name=TypeId, Index=1
                  PropertyChange, Path=Tables/Metadata.Extension/Columns/TypeId, Type=Type: Int32, Length: null
                GroupingNode, Comment=Text
                  CreateNode, Path=Tables/Metadata.Extension, Type=ColumnInfo, Name=Text, Index=2
                  PropertyChange, Path=Tables/Metadata.Extension/Columns/Text, Type=Type: String?, Length: null
                GroupingNode, Comment=Data
                  CreateNode, Path=Tables/Metadata.Extension, Type=ColumnInfo, Name=Data, Index=3
                  PropertyChange, Path=Tables/Metadata.Extension/Columns/Data, Type=Type: Byte[]?, Length: null
              GroupingNode, Comment=PK_Extension
                CreateNode, Path=Tables/Metadata.Extension, Type=PrimaryIndexInfo, Name=PK_Extension
                PropertyChange, Path=Tables/Metadata.Extension/PrimaryIndex, IsUnique=True
                GroupingNode, Comment=KeyColumns[]
                  GroupingNode, Comment=0
                    CreateNode, Path=Tables/Metadata.Extension/PrimaryIndex, Type=KeyColumnRef, Name=0, Index=0
                    PropertyChange, Path=Tables/Metadata.Extension/PrimaryIndex/KeyColumns/0, Value=Tables/Metadata.Extension/Columns/Name, Direction=Positive
            GroupingNode, Comment=Metadata.Assembly
              CreateNode, Path=, Type=TableInfo, Name=Metadata.Assembly, Index=2
              GroupingNode, Comment=Columns[]
                GroupingNode, Comment=Name
                  CreateNode, Path=Tables/Metadata.Assembly, Type=ColumnInfo, Name=Name, Index=0
                  PropertyChange, Path=Tables/Metadata.Assembly/Columns/Name, Type=Type: String, Length: 1024
                GroupingNode, Comment=TypeId
                  CreateNode, Path=Tables/Metadata.Assembly, Type=ColumnInfo, Name=TypeId, Index=1
                  PropertyChange, Path=Tables/Metadata.Assembly/Columns/TypeId, Type=Type: Int32, Length: null
                GroupingNode, Comment=Version
                  CreateNode, Path=Tables/Metadata.Assembly, Type=ColumnInfo, Name=Version, Index=2
                  PropertyChange, Path=Tables/Metadata.Assembly/Columns/Version, Type=Type: String?, Length: 64
              GroupingNode, Comment=PK_Assembly
                CreateNode, Path=Tables/Metadata.Assembly, Type=PrimaryIndexInfo, Name=PK_Assembly
                PropertyChange, Path=Tables/Metadata.Assembly/PrimaryIndex, IsUnique=True
                GroupingNode, Comment=KeyColumns[]
                  GroupingNode, Comment=0
                    CreateNode, Path=Tables/Metadata.Assembly/PrimaryIndex, Type=KeyColumnRef, Name=0, Index=0
                    PropertyChange, Path=Tables/Metadata.Assembly/PrimaryIndex/KeyColumns/0, Value=Tables/Metadata.Assembly/Columns/Name, Direction=Positive
            GroupingNode, Comment=MyEntity
              CreateNode, Path=, Type=TableInfo, Name=MyEntity, Index=3
              GroupingNode, Comment=Columns[]
                GroupingNode, Comment=Id
                  CreateNode, Path=Tables/MyEntity, Type=ColumnInfo, Name=Id, Index=0
                  PropertyChange, Path=Tables/MyEntity/Columns/Id, Type=Type: Int32, Length: null
                GroupingNode, Comment=TypeId
                  CreateNode, Path=Tables/MyEntity, Type=ColumnInfo, Name=TypeId, Index=1
                  PropertyChange, Path=Tables/MyEntity/Columns/TypeId, Type=Type: Int32, Length: null
                GroupingNode, Comment=Text
                  CreateNode, Path=Tables/MyEntity, Type=ColumnInfo, Name=Text, Index=2
                  PropertyChange, Path=Tables/MyEntity/Columns/Text, Type=Type: String?, Length: 100
              GroupingNode, Comment=PK_MyEntity
                CreateNode, Path=Tables/MyEntity, Type=PrimaryIndexInfo, Name=PK_MyEntity
                PropertyChange, Path=Tables/MyEntity/PrimaryIndex, IsUnique=True
                GroupingNode, Comment=KeyColumns[]
                  GroupingNode, Comment=0
                    CreateNode, Path=Tables/MyEntity/PrimaryIndex, Type=KeyColumnRef, Name=0, Index=0
                    PropertyChange, Path=Tables/MyEntity/PrimaryIndex/KeyColumns/0, Value=Tables/MyEntity/Columns/Id, Direction=Positive
          GroupingNode, Comment=Sequences[]
            GroupingNode, Comment=Int32-Generator
              CreateNode, Path=, Type=SequenceInfo, Name=Int32-Generator, Index=0
              PropertyChange, Path=Sequences/Int32-Generator, Increment=128
      GroupingNode, Comment=DataManipulate
      GroupingNode, Comment=Cleanup
      
    Session 'System, #1'. Schema upgrade script:
    CREATE TABLE [dbo].[Metadata.Type] ([Id] integer NOT NULL,
    [TypeId] integer NOT NULL,
    [Name] nvarchar(1000) DEFAULT NULL,
    CONSTRAINT [PK_Type] PRIMARY KEY ([Id]));
    CREATE UNIQUE INDEX [Type.IX_Name] ON [dbo].[Metadata.Type] ([Name] ASC)  INCLUDE ([TypeId]);
    CREATE TABLE [dbo].[Metadata.Extension] ([Name] nvarchar(1024) NOT NULL,
    [TypeId] integer NOT NULL,
    [Text] nvarchar(max) DEFAULT NULL,
    [Data] varbinary(max) DEFAULT NULL,
    CONSTRAINT [PK_Extension] PRIMARY KEY ([Name]));
    CREATE TABLE [dbo].[Metadata.Assembly] ([Name] nvarchar(1024) NOT NULL,
    [TypeId] integer NOT NULL,
    [Version] nvarchar(64) DEFAULT NULL,
    CONSTRAINT [PK_Assembly] PRIMARY KEY ([Name]));
    CREATE TABLE [dbo].[MyEntity] ([Id] integer NOT NULL,
    [TypeId] integer NOT NULL,
    [Text] nvarchar(100) DEFAULT NULL,
    CONSTRAINT [PK_MyEntity] PRIMARY KEY ([Id]));
    CREATE TABLE [dbo].[Int32-Generator] ([ID] integer IDENTITY (128, 128) NOT NULL);
    Session 'n/a'. SQL batch: 
    CREATE TABLE [dbo].[Metadata.Type] ([Id] integer NOT NULL,
    [TypeId] integer NOT NULL,
    [Name] nvarchar(1000) DEFAULT NULL,
    CONSTRAINT [PK_Type] PRIMARY KEY ([Id]));
    CREATE UNIQUE INDEX [Type.IX_Name] ON [dbo].[Metadata.Type] ([Name] ASC)  INCLUDE ([TypeId]);
    CREATE TABLE [dbo].[Metadata.Extension] ([Name] nvarchar(1024) NOT NULL,
    [TypeId] integer NOT NULL,
    [Text] nvarchar(max) DEFAULT NULL,
    [Data] varbinary(max) DEFAULT NULL,
    CONSTRAINT [PK_Extension] PRIMARY KEY ([Name]));
    CREATE TABLE [dbo].[Metadata.Assembly] ([Name] nvarchar(1024) NOT NULL,
    [TypeId] integer NOT NULL,
    [Version] nvarchar(64) DEFAULT NULL,
    CONSTRAINT [PK_Assembly] PRIMARY KEY ([Name]));
    CREATE TABLE [dbo].[MyEntity] ([Id] integer NOT NULL,
    [TypeId] integer NOT NULL,
    [Text] nvarchar(100) DEFAULT NULL,
    CONSTRAINT [PK_MyEntity] PRIMARY KEY ([Id]));
    CREATE TABLE [dbo].[Int32-Generator] ([ID] integer IDENTITY (128, 128) NOT NULL);
    Synchronizing schema in Recreate mode: completed.
    Creating Generators: started.
    Creating Generators: completed.
    Session 'System, #1'. SQL batch: 
    SELECT [a].[Name], [a].[TypeId], [a].[Version] FROM [dbo].[Metadata.Assembly] [a];
    Session 'System, #1'. Caching: Key = 'Assembly, (Xtensive.Storage)', Tuple = (Xtensive.Storage, 2, null), State = New.
    Session 'System, #1'. Materializing Assembly: Key = 'Assembly, (Xtensive.Storage)'.
    Session 'System, #1'. Setting value: Key = 'Assembly, (Xtensive.Storage)', Field = 'Version (FieldInfo)'.
    Session 'System, #1'. Getting value: Key = 'Assembly, (Xtensive.Storage)', Field = 'Version (FieldInfo)'.
    Session 'System, #1'. Getting value: Key = 'Assembly, (Xtensive.Storage)', Field = 'Name (FieldInfo)'.
    Session 'System, #1'. Getting value: Key = 'Assembly, (Xtensive.Storage)', Field = 'Version (FieldInfo)'.
    Metadata.Assembly created: 'Xtensive.Storage (Version=1.0.0.0)'.
    Session 'System, #1'. Caching: Key = 'Assembly, (Batching)', Tuple = (Batching, 2, null), State = New.
    Session 'System, #1'. Materializing Assembly: Key = 'Assembly, (Batching)'.
    Session 'System, #1'. Setting value: Key = 'Assembly, (Batching)', Field = 'Version (FieldInfo)'.
    Session 'System, #1'. Getting value: Key = 'Assembly, (Batching)', Field = 'Version (FieldInfo)'.
    Session 'System, #1'. Getting value: Key = 'Assembly, (Batching)', Field = 'Name (FieldInfo)'.
    Session 'System, #1'. Getting value: Key = 'Assembly, (Batching)', Field = 'Version (FieldInfo)'.
    Metadata.Assembly created: 'Batching (Version=1.0.0.0)'.
    Session 'System, #1'. Persisting (partial)...
    Session 'System, #1'. Persist completed.
    Session 'System, #1'. SQL batch: 
    INSERT INTO [dbo].[Metadata.Assembly] ([Name], [TypeId], [Version]) VALUES (@p0_0, @p0_1, @p0_2);
    INSERT INTO [dbo].[Metadata.Assembly] ([Name], [TypeId], [Version]) VALUES (@p1_0, @p1_1, @p1_2);
    SELECT [a].[Id], [a].[TypeId], [a].[Name] FROM [dbo].[Metadata.Type] [a];
    Session 'System, #1'. Caching: Key = 'Type, (1)', Tuple = (1, 1, null), State = New.
    Session 'System, #1'. Materializing Type: Key = 'Type, (1)'.
    Session 'System, #1'. Setting value: Key = 'Type, (1)', Field = 'Name (FieldInfo)'.
    Session 'System, #1'. Getting value: Key = 'Type, (1)', Field = 'Name (FieldInfo)'.
    Session 'System, #1'. Caching: Key = 'Type, (3)', Tuple = (3, 1, null), State = New.
    Session 'System, #1'. Materializing Type: Key = 'Type, (3)'.
    Session 'System, #1'. Setting value: Key = 'Type, (3)', Field = 'Name (FieldInfo)'.
    Session 'System, #1'. Getting value: Key = 'Type, (3)', Field = 'Name (FieldInfo)'.
    Session 'System, #1'. Caching: Key = 'Type, (2)', Tuple = (2, 1, null), State = New.
    Session 'System, #1'. Materializing Type: Key = 'Type, (2)'.
    Session 'System, #1'. Setting value: Key = 'Type, (2)', Field = 'Name (FieldInfo)'.
    Session 'System, #1'. Getting value: Key = 'Type, (2)', Field = 'Name (FieldInfo)'.
    Session 'System, #1'. Caching: Key = 'Type, (101)', Tuple = (101, 1, null), State = New.
    Session 'System, #1'. Materializing Type: Key = 'Type, (101)'.
    Session 'System, #1'. Setting value: Key = 'Type, (101)', Field = 'Name (FieldInfo)'.
    Session 'System, #1'. Getting value: Key = 'Type, (101)', Field = 'Name (FieldInfo)'.
    Session 'System, #1'. Persisting (partial)...
    Session 'System, #1'. Persist completed.
    Session 'System, #1'. SQL batch: 
    INSERT INTO [dbo].[Metadata.Type] ([Id], [TypeId], [Name]) VALUES (@p0_0, @p0_1, @p0_2);
    INSERT INTO [dbo].[Metadata.Type] ([Id], [TypeId], [Name]) VALUES (@p1_0, @p1_1, @p1_2);
    INSERT INTO [dbo].[Metadata.Type] ([Id], [TypeId], [Name]) VALUES (@p2_0, @p2_1, @p2_2);
    INSERT INTO [dbo].[Metadata.Type] ([Id], [TypeId], [Name]) VALUES (@p3_0, @p3_1, @p3_2);
    SELECT TOP 2 [a].[Name], [a].[TypeId], [a].[Text], [a].[Data] FROM [dbo].[Metadata.Extension] [a] WHERE ([a].[Name] = 'Xtensive.Storage.Model') ORDER BY [a].[Name] ASC;
    Session 'System, #1'. Caching: Key = 'Extension, (Xtensive.Storage.Model)', Tuple = (Xtensive.Storage.Model, 3, null, null), State = New.
    Session 'System, #1'. Materializing Extension: Key = 'Extension, (Xtensive.Storage.Model)'.
    Session 'System, #1'. Setting value: Key = 'Extension, (Xtensive.Storage.Model)', Field = 'Text (FieldInfo)'.
    Session 'System, #1'. Getting value: Key = 'Extension, (Xtensive.Storage.Model)', Field = 'Text (FieldInfo)'.
    Session 'System, #1'. Persisting (full)...
    Session 'System, #1'. SQL batch: 
    INSERT INTO [dbo].[Metadata.Extension] ([Name], [TypeId], [Text], [Data]) VALUES (@p0_0, @p0_1, @p0_2, @p0_3);
    Session 'System, #1'. Persist completed.
    Session 'System, #1'. Commit transaction.
    Session 'System, #1'. Disposing.
    Session 'System, #1'. Closing connection 'sqlserver://localhost/DO40-Tests'.
    Building Domain: completed.
    Opening session 'Name = Default, Options = Default, CacheType = LruWeak, CacheSize = 16384, DefaultIsolationLevel = ReadCommitted'.
    Session 'Default, #2'. Creating connection 'sqlserver://localhost/DO40-Tests'.
    Session 'Default, #2'. Opening connection 'sqlserver://localhost/DO40-Tests'.
    Session 'Default, #2'. Beginning transaction @ ReadCommitted.
    Opening session 'Name = KeyGenerator, Options = Default, CacheType = LruWeak, CacheSize = 16384, DefaultIsolationLevel = ReadCommitted'.
    Session 'KeyGenerator, #3'. Creating connection 'sqlserver://localhost/DO40-Tests'.
    Session 'KeyGenerator, #3'. Opening connection 'sqlserver://localhost/DO40-Tests'.
    Session 'KeyGenerator, #3'. Beginning transaction @ ReadCommitted.
    Session 'KeyGenerator, #3'. SQL batch: 
    INSERT INTO [dbo].[Int32-Generator] DEFAULT VALUES;
    SELECT SCOPE_IDENTITY();
    Session 'KeyGenerator, #3'. Commit transaction.
    Session 'KeyGenerator, #3'. Disposing.
    Session 'KeyGenerator, #3'. Closing connection 'sqlserver://localhost/DO40-Tests'.
    Session 'Default, #2'. Caching: Key = 'MyEntity, (1)', Tuple = (1, 101, null), State = New.
    Session 'Default, #2'. Materializing MyEntity: Key = 'MyEntity, (1)'.
    Session 'Default, #2'. Setting value: Key = 'MyEntity, (1)', Field = 'Text (FieldInfo)'.
    Session 'Default, #2'. Getting value: Key = 'MyEntity, (1)', Field = 'Text (FieldInfo)'.
    Session 'Default, #2'. Caching: Key = 'MyEntity, (2)', Tuple = (2, 101, null), State = New.
    Session 'Default, #2'. Materializing MyEntity: Key = 'MyEntity, (2)'.
    Session 'Default, #2'. Setting value: Key = 'MyEntity, (2)', Field = 'Text (FieldInfo)'.
    Session 'Default, #2'. Getting value: Key = 'MyEntity, (2)', Field = 'Text (FieldInfo)'.
    Session 'Default, #2'. Persisting (partial)...
    Session 'Default, #2'. Persist completed.
    Session 'Default, #2'. SQL batch: 
    INSERT INTO [dbo].[MyEntity] ([Id], [TypeId], [Text]) VALUES (@p0_0, @p0_1, @p0_2);
    INSERT INTO [dbo].[MyEntity] ([Id], [TypeId], [Text]) VALUES (@p1_0, @p1_1, @p1_2);
    SELECT COUNT_BIG(*) AS [column] FROM [dbo].[MyEntity] [a];
    SELECT [a].[Id], [a].[TypeId], [a].[Text] FROM [dbo].[MyEntity] [a];
    Session 'Default, #2'. Updating cache: Key = 'MyEntity, (1)', Tuple = (1, 101, Entity 1), State = Synchronized.
    Session 'Default, #2'. Getting value: Key = 'MyEntity, (1)', Field = 'Text (FieldInfo)'.
    Session 'Default, #2'. Updating cache: Key = 'MyEntity, (2)', Tuple = (2, 101, Entity 2), State = Synchronized.
    Session 'Default, #2'. Getting value: Key = 'MyEntity, (2)', Field = 'Text (FieldInfo)'.
    Session 'Default, #2'. Commit transaction.
    Session 'Default, #2'. Disposing.
    Session 'Default, #2'. Closing connection 'sqlserver://localhost/DO40-Tests'.
    
    Really long, but there are lots of interesting things. But for now I'd like to pay your attention to just a single aspect. This log perfectly shows how our uber-batching and future queries work.

    First part to pay attention to this batch:
    -- Session 'System, #1'. SQL batch: 
    INSERT INTO [dbo].[Metadata.Type] ([Id], [TypeId], [Name]) VALUES (@p0_0, @p0_1, @p0_2);
    INSERT INTO [dbo].[Metadata.Type] ([Id], [TypeId], [Name]) VALUES (@p1_0, @p1_1, @p1_2);
    INSERT INTO [dbo].[Metadata.Type] ([Id], [TypeId], [Name]) VALUES (@p2_0, @p2_1, @p2_2);
    INSERT INTO [dbo].[Metadata.Type] ([Id], [TypeId], [Name]) VALUES (@p3_0, @p3_1, @p3_2);
    SELECT TOP 2 [a].[Name], [a].[TypeId], [a].[Text], [a].[Data] FROM [dbo].[Metadata.Extension] [a] WHERE ([a].[Name] = 'Xtensive.Storage.Model') ORDER BY [a].[Name] ASC;
    
    You may find this batch is produced by the following code from our SystemUpgradeHandler class:
    domainModel.Types
      .Where(type => type.TypeId!=TypeInfo.NoTypeId)
      .Apply(type => new M.Type(type.TypeId, type.UnderlyingType.GetFullName()));
    
    ...
    
    var modelHolder = Query<Extension>.All.SingleOrDefault(
      extension => extension.Name==WellKnown.DomainModelExtensionName);
    
    So in fact, it executes 5 different commands. So 5 separate roundtrips is nearly what you would get with EF and many other ORM tools. AFAIK, you would see 2 roundtrips in the best case (if CUD sequence batching is implemented). In our case there is just a single roundtrip!

    Note how gracefully we handle SingleOrDefault(): our query translator has added TOP 2 (think why 2 :) ) and ORDER BY to make TOP work.

    The second batch is formed by code inside using block of Main method of Program class:
    -- Session 'Default, #2'. SQL batch: 
    INSERT INTO [dbo].[MyEntity] ([Id], [TypeId], [Text]) VALUES (@p0_0, @p0_1, @p0_2);
    INSERT INTO [dbo].[MyEntity] ([Id], [TypeId], [Text]) VALUES (@p1_0, @p1_1, @p1_2);
    SELECT COUNT_BIG(*) AS [column] FROM [dbo].[MyEntity] [a];
    SELECT [a].[Id], [a].[TypeId], [a].[Text] FROM [dbo].[MyEntity] [a];
    
    Isn't this beautiful? You make a set of CUD commands and queries, but DO4 runs all of them in a single roundtrip! Normally you shouldn't touch your code at all to get this!

    So DO4 reduces chattiness between application server and RDBMS to nearly minimal possible level.

    You may find that there are 2 more batches.
    • First one updates [Metadata.Extension] table: actually it persists serialized storage model there
    • Second one allocates next bulk of keys from integer key generator (128 at once by default).
    Now let's talk a bit about logging. With today's nightly build it is possible to use it without any third-party logger, such as Log4Net. By default it writes everything to Debug output, if debugger is attached to the current process. Note that default log format is more comprehensive - if you completely remove <logs> section (or e.g. remove formatString attribute), you should see nearly the following output:
      0,09s/T1      Info Storage.Building         Building Domain: started.
      0,09s/T1      Info Storage.Building         Creating Domain: started.
      0,12s/T1      Info Storage.Building         Creating Domain: completed.
    ...
      2,57s/T1     Debug Storage.Providers.Sql    Session 'KeyGenerator, #3'. Beginning transaction @ ReadCommitted.
      2,57s/T1     Debug Storage.Providers.Sql    Session 'KeyGenerator, #3'. SQL batch: 
                                                  INSERT INTO [dbo].[Int32-Generator] DEFAULT VALUES;
                                                  SELECT SCOPE_IDENTITY();
      2,57s/T1     Debug Storage.Providers.Sql    Session 'KeyGenerator, #3'. Commit transaction.
    ...
    
    So even integrated log providers are very flexible. You can choose:
    • Which logs (by namespaces they're located in; "Xtensive." prefix is cut out) must be directed to which providers (provider attribute value can be Null, Console, Error, Debug, File; fileName attribute allows to specify file name)
    • Which event types must be shown there (events attribute allows to do this).
    • Pre-defined format: format attribute value can be Simple or Comprehensive.
    • Custom format string: use formatString attribute to set it. Comprehensive format string is "{0,6:F2}s/T{1,-5} {2,5} {3,-24} {5}".
    Here is more comprehensive example of logs section:
    <!-- Log nothing by default -->
    <log name=""
         provider="Null" />
    <!-- But use these settings for Xtensive.Core.* logs -->
    <log name="Core"
         provider="Console" events="Warning,Error,FatalError" format="Simple" />
    <!-- This overrides settings for Core.Diagnistics.* logs -->
    <log name="Core.Diagnistics" 
         provider="Console" format="{4}{5}" />
    <!-- Use these settings for Xtensive.Storage.* logs -->
    <log name="Storage" 
         provider="File" fileName="Storage.log" />
    
    Note: you need today's nightly build to make new logging stuff work. We added such integrated log providers just today.

    P.S. Try to study the log - this might explain lots of aspects of our framework.

    October 13, 2009

    DataObjects.Net v4.1 RC is available

    Finally it's done: v4.1 Release Candidate is here.

    Official release announcement will be published shortly, as well as roadmap, revision history and issues in issue tracker. I should say we were unable to accomplish the following planned tasks:
    • There is no Manual for now. It will appear on the next week.
    • DisconnectedState isn't fully operational yet, and thus there is no demo. We hope to finish with this on the next week as well.
    • Prefetch API works, but queries it sends are far from ideal yet. They'll be ideal when we'll finish with  automatic fall back to IN for local collections. Currently they're always processed via temporary tables. Although new API already improves performance of raw fetches by nearly 3 times (shortly it will be ~ 10 more times faster), and significantly improves multiple EntitySet access performance.
    • There is no ASP.NET MVC sample yet, but it will be there soon as well.
    Everything else works as we promised. There was a real hell with persistent interfaces, and frankly speaking, we spent much more time on this in comparison to our initial estimates.

    We also slightly changed our samples:
    • ASP.NET sample handles concurrent update checks relying on our new Entity.GetVersion() method. To test this, try to edit the same entity from two different pages.
    • WPF sample got skin and WPF validation support (now we support IDataErrorInfo).
    Finally, there is much better installer and integration with MSBuild:
    • No need to install PostSharp or something else
    • No more installation into GAC, so your bin folders contain binaries fully ready for deployment with XCopy
    • We use new way of integration with MSBuild allowing you to make a project based on DO4 fully "XCopyable" - you won't need to install DO4 to compile it on another PC.
    • There are new project templates - they're much better for beginners
    • You can copy DO4 to another PC without installer now. Just XCopy it and run Install.bat; Uninstall.bat will remove it.
    Notes for v4.X users: 
    • You must modify your existing .csproj files while switching to this version.
    • Uninstall all third-party tools installed by previous versions of DO4 before switching to v4.1, including PostSharp.
    Go and try it now!

    Nightly builds are back!

    That's because my last month's work on integration with MSBuild and new installer is finished :) If you will face any issues with new MSBuild integration approach, please notify me.

    October 11, 2009

    "Death by PowerPoint, and how to fight it" - the link

    Here are few rules that might help you to not fail with PowerPoint presentation.

    The content there seems really helpful, so I decided to publish the link here.

    DataObjects.Net v4.1: Release Candidate (RC) instead of Release on Monday

    Finally we decided to call the version we're going to show on Monday as Release Candidate. Guys from my team thinks this is honest:

    • It passes all the test we had earlier (~ 1000 tests for Storage + 1000 tests for everything else), as well as ~ 100 new tests.
    • We have fixed all the bugs found in v4.0.5 there. We run continuous integration + unit testing and measure code coverage, so we're fully sure new build is more stable than v4.0.5.
    • New features provided there aren't tested so well. There are lots of them, but they're covered by ~ 100 new tests. That's the main reason we call it as RC. Another one is that implementation of some new features isn't optimal yet. Later I'll describe what we're going to improve.
    So I recommend switching to this version immediately after its appearance, although you must be ready for some surprises from new parts of API.

    Adding DataObjects.Net to .csproj: the new way

    DataObjects.Net v4.1 offers a very simple way of adding it to DataObjects.Net. Let's take a look on typical .csproj file for it:
    <?xml version="1.0" encoding="utf-8"?>
    <Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
      <PropertyGroup>
        ...
        <!-- Optional, default is false: -->
        <UsePostSharp>true</UsePostSharp>
        <!-- Required, see the description below: -->
        <DontImportPostSharp>true</DontImportPostSharp>
        <!-- Optional, default is false: -->
        <UseFiXml>true</UseFiXml>
        <!-- Optional, default is true: -->
        <CopyIndirectDependencies>false</CopyIndirectDependencies>
        <!-- Optional, default is environment variable value -->
        <DataObjectsDotNetPath>..\DO4</DataObjectsDotNetPath>
      </PropertyGroup>
      <ItemGroup>
        ...
        <Reference Include="PostSharp.Laos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b13fd38b8f9c99d7, processorArchitecture=MSIL" />
        <Reference Include="PostSharp.Public, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b13fd38b8f9c99d7, processorArchitecture=MSIL" />
        <Reference Include="Xtensive.Core, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
          <SpecificVersion>False</SpecificVersion>
        </Reference>
        <Reference Include="Xtensive.Core.Aspects, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
          <SpecificVersion>False</SpecificVersion>
        </Reference>
        <Reference Include="Xtensive.Indexing, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
          <SpecificVersion>False</SpecificVersion>
        </Reference>
        <Reference Include="Xtensive.Integrity, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
          <SpecificVersion>False</SpecificVersion>
        </Reference>
        <Reference Include="Xtensive.Storage, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
          <SpecificVersion>False</SpecificVersion>
        </Reference>
        <Reference Include="Xtensive.Storage.Model, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
          <SpecificVersion>False</SpecificVersion>
        </Reference>
        <Reference Include="Xtensive.Storage.All, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
          <SpecificVersion>False</SpecificVersion>
        </Reference>
      </ItemGroup>
      ...
      <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
      <Import Project="$(DataObjectsDotNetPath)\Common\DataObjects.Net.targets" />
      ...
    </Project>
    
    Let's discuss each particular line here.

    Properties:
    • UsePoshSharp property indicates whether PostSharp must be applied to this assembly. This option is necessary only for projects declaring persistent types (Entity, Structure or SessionBound descendants), but not for projects using them (in these cases it is optional).
    • DontImportPostSharp property indicates any separately installed PostSharp version (if any) must not be used. This is a required option - if you won't add corresponding line, your project won't compile on machine where PostSharp is installed with "Global PostSharp Auto-Detection" option. Installed version of PostSharp will detect indirect reference to it from any project that references DataObjects.Net assemblies (directly or indirectly), and will try to post-process its binary; but this is impossible, since now we don't install our weaver into your own PostSharp installation - it is integrated into PostSharp version we ship in Lib folder, that is used by DataObjects.Net.targets.
    • UseFiXml property turns on post-processing of XML comments for this project by FiXml tool. If you'd like to use <inheritdoc/> and <see ... copy="true"/> inside your documentation, just set it to true.
    • CopyIndirectDependencies ensures copying of all indirect dependencies to project output folder. This post describes why this is so convenient. Set it to false to disable this: default value of this property is true.
    • DataObjectsDotNetPath property may specify relative or absolute path to DataObjects.Net. If it is omitted (that's default case), value of the same environment variable will be used. This environment variable is defined by new Install.bat (and thus by installer as well). So now it is really easy to make your DataObjects.Net-based solution compilable after XCopy: either run <DataObjects.Net root folder>\Install\Install.bat on the target machine after XCopying, or specify relative path to DataObjects.Net root folder in .csproj/.vbproj with this option (this implies you must keep Common, Bin and Lib folders from  DataObjects.Net root folder inside your solution).
    References:
    • To PostSharp.* assemblies. They're necessary only for assemblies containing persistent types, since our aspects are inherited from types declared there. As you see, there is no necessity to specify correct <HintPath> now: appropriate search path will be provided by DataObjects.Net.targets.
    • To Xtensive.* assemblies. They're necessary in almost any project using DataObjects.Net, since your own persistent types are inherited from types declared there. Again, there is no necessity to specify correct <HintPath> now: appropriate search path will be provided by DataObjects.Net.targets.
    • To Xtensive.Storage.All.dll. This assembly indirectly references all the assemblies that can be loaded by DataObjects.Net in any possible configuration. So for example, it indirectly references Mono.Security.dll, which is referenced by NPgSql.dll, which is loaded when DataObjects.Net is used with PostgreSQL provider. Referencing Xtensive.Storage.All.dll in conjunction with CopyIndirectDependencies option ensures output folder will contain all the assemblies necessary to run your application in any possible configuration.
    Imports:
    • $(DataObjectsDotNetPath)\Common\DataObjects.Net.targets. This .targets file does all the magic that stands behind all the properties I just described. As you already know, its location depends on DataObjectsDotNetPath property value or the same environment variable.
    New project template files contains these additional lines. If you're using Visual Basic .NET, the changes you should make to .vbproj file are absolutely the same.

    New installer: Install.bat

    Likely, you don't know that I like .bat files ;) Obviously, not because they're good as programming language (they're really bad!), but because they provide petty flexible ways of doing various management tasks. Initial learning curve here is tiny, but you must write lots of them to be a ".bat magician".

    Ok, let's return back to the post title. Today I wrote a .bat file handling the whole DO4 installation! So you can take our repository snapshot, run it, and... VoilĂ : everything works. XCopy + Install.bat.

    All you need to get DataObjects.Net checked out, compiled and installed after its shortcoming appearance at Google Code is:
    hg clone https://dataobjectsdotnet.googlecode.com/hg/ DataObjects.Net
    call "DataObjects.Net\Install\Install.bat" -a -b
    
    • -a option here implies all installation procedures must be performed rather than just basic one. The "Install.bat -?" output below explains this.
    • -b option means DataObjects.Net must be built as well.
    The output of Install.bat in this case is:
    C:\My Projects\Svn>"Xtensive\Install\Install.bat" -a -b
    Updating .NET assembly folders:
      Adding paths to DataObjects.Net binaries and components.
      Done.
    Installing Visual Studio .NET project templates:
      Visual Studio .NET 2008 paths:
        Binaries:  C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\.
        Templates: C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\ProjectTemplates
      Installing CSharp templates for Visual Studio .NET 2008:
        Adding template: Console
        Adding template: Model
        Adding template: UnitTests
        Adding template: WebApplication
        Adding template: Wpf
      Registering changes in Visual Studio .NET 2008...
      Done.
    Installing DataObjects.Net samples:
      Installing Xtensive.Storage.Samples.AspNet
    Can't create 'DO4AspNetSample': virtual folder already exists.
      Done.
    Adding DataObjects.Net help to Visual Studio .NET help collection:
      Done.
    Adding DataObjects.Net .targets to `safe imports':
      Updating registry keys for Visual Studio .NET 2008...
      Done.
    Installing DataObjects.Net environment variable(s):
      Adding: DataObjectsDotNetPath=C:\My Projects\Svn\Xtensive
      Done.
    Note: new settings will affect on new processes,
          but not on the current one!
    Building DataObjects.Net:
      Cleaning up...
      Building Core (Release), pass 1 (ensures PostSharp weaver is built)...
      Building Core (Release), pass 2 (ensures Core.Aspacts assembly is built)...
      Building Storage (Release)...
      Building Samples (Release)...
      Done.
    
    As you see, Install.bat performs all the actions necessary to install or uninstall DataObjects.Net (there is -u switch, or you can use Uninstall.bat).

    Its quick reference (Install.bat -?):
    Usage: [Un]Install.bat [arguments]
    Arguments:
           -u: uninstall everything. The same as typing Uninstall.bat.
           -a: install everything.
               Implies every option except -u and -b.
           -r: add paths to DataObjects.Net assemblies to
               .NET assembly folders. This affects on "Add reference"
               dialog in Visual Studio .NET.
           -t: install Visual Studio .NET templates.
           -s: install samples: create virtual folders, etc.
           -h: install help: integrate it into
               Visual Studio .NET help collection.
           -b: build DataObjects.Net, Release configuration.
          -bd: build DataObjects.Net, Debug configuration.
         none: install DataObjects.Net environment variable and
               mark DataObjects.Net .target files as `safe
               to import' in Visual Studio .NET.
    
    Appearance of Install.bat does not imply disappearance of regular installer! It will be nearly the same, but most of actions that were scripted in it earlier are now moved to Install.bat.