The key concepts of this example:

  • It shows how to work with referances and collections – as you will see, DataObjects.Net handles relations between objects transparently.
  • It uses set of special attributes helping to tune up the collections in the needed way – [PairTo], [Contained], [Transitive], [SelfReferenceAllowed].
  • It performs querying – from simplest queries to complex ones, including collection and full-text queries.
  • It shows one of the simplest ways to populate composite full-text index data.
  • It uses enumeration with [Flags] attribute as persistent property type.
  • It uses custom type (BatInfo) as persistent property type. Note that BatInfo holds a reference to persistent instance. Also BatInfo implements IDataObjectField allowing to notify DataObjects.Net on property content changes.

The system of persistent types used in this example is following:

DataObject
  FtObject
    Animal (IDumpable)
      HomeAnimal
        Cat
      Bat

The instances of Cat and Bat types will be created in this example. Following the tradition we put the description of „how it works“ into the comments in the example. Notice that Microsoft Search should be installed and running to perform this example.

Sample output

DataObjects.Net: Animals demo

Select database server to use:
1) Microsoft SQL Server
2) Oracle
3) Native Oracle
4) SAP DB
> 1
Selected: Microsoft SQL Server.
Reading product key...

Building domain...
Connection URL: mssql://localhost/DataObjectsDotNetDemos
Driver:         Microsoft SQL Server\MSDE database driver for DataObjects.Net
Storage content:
SecurityRoot, ID=1
Role, ID=2
Role, ID=3
StdUser, ID=4
StdUser, ID=5
Cat, ID=6
  Age:        4
  Leg count:  4
  Parent:     Cat, ID=9
  Children:   1
    Cat, ID=7
  Name:       Sima
  Friends:    2
    Cat, ID=7, Name=Sonya
    Cat, ID=8, Name=Tom
  Colors:     Gray
Cat, ID=7
  Age:        2
  Leg count:  4
  Parent:     Cat, ID=6
  Name:       Sonya
  Friends:    2
    Cat, ID=6, Name=Sima
    Cat, ID=8, Name=Tom
  Colors:     Black, White
Cat, ID=8
  Age:        3
  Leg count:  4
  Parent:     Cat, ID=9
  Name:       Tom
  Friends:    3
    Cat, ID=7, Name=Sonya
    Cat, ID=6, Name=Sima
    Cat, ID=8, Name=Tom
  Colors:     Black, Other
Cat, ID=9
  Age:        Unknown
  Leg count:  4
  Children:   2
    Cat, ID=6
    Cat, ID=8
  Name:       Pam
  Colors:     Other
End.

Storage content:
SecurityRoot, ID=1
Role, ID=2
Role, ID=3
StdUser, ID=4
StdUser, ID=5
End.

Storage content:
SecurityRoot, ID=1
Role, ID=2
Role, ID=3
StdUser, ID=4
StdUser, ID=5
Cat, ID=6
  Age:        4
  Leg count:  4
  Parent:     Cat, ID=9
  Children:   1
    Cat, ID=7
  Name:       Sima
  Friends:    2
    Cat, ID=7, Name=Sonya
    Cat, ID=8, Name=Tom
  Colors:     Gray
Cat, ID=7
  Age:        2
  Leg count:  4
  Parent:     Cat, ID=6
  Name:       Sonya
  Friends:    2
    Cat, ID=6, Name=Sima
    Cat, ID=8, Name=Tom
  Colors:     Black, White
Cat, ID=8
  Age:        3
  Leg count:  4
  Parent:     Cat, ID=9
  Name:       Tom
  Friends:    3
    Cat, ID=6, Name=Sima
    Cat, ID=7, Name=Sonya
    Cat, ID=8, Name=Tom
  Colors:     Black, Other
Cat, ID=9
  Age:        Unknown
  Leg count:  4
  Children:   2
    Cat, ID=6
    Cat, ID=8
  Name:       Pam
  Colors:     Other
End.

2-legged Animals:
Query result:
Bat, ID=10
  Age:        2
  Leg count:  2
  Parent:     Bat, ID=10
  Children:   1
    Bat, ID=10
  Info.Text:  This bat is quite clever.
  Info.Mate:  Bat, ID=10
End.

Animals with Age>2:
Query result:
Cat, ID=6
  Age:        4
  Leg count:  4
  Parent:     Cat, ID=9
  Children:   1
    Cat, ID=7
  Name:       Sima
  Friends:    2
    Cat, ID=7, Name=Sonya
    Cat, ID=8, Name=Tom
  Colors:     Gray
Cat, ID=8
  Age:        3
  Leg count:  4
  Parent:     Cat, ID=9
  Name:       Tom
  Friends:    3
    Cat, ID=6, Name=Sima
    Cat, ID=7, Name=Sonya
    Cat, ID=8, Name=Tom
  Colors:     Black, Other
End.

Grandparents:
Query result:
Cat, ID=9
  Age:        Unknown
  Leg count:  4
  Children:   2
    Cat, ID=6
    Cat, ID=8
  Name:       Pam
  Colors:     Other
Bat, ID=10
  Age:        2
  Leg count:  2
  Parent:     Bat, ID=10
  Children:   1
    Bat, ID=10
  Info.Text:  This bat is quite clever.
  Info.Mate:  Bat, ID=10
End.

Waiting for full-text index population.....................
Query result:
Cat, ID=8
  Age:        3
  Leg count:  4
  Parent:     Cat, ID=9
  Name:       Tom
  Friends:    3
    Cat, ID=6, Name=Sima
    Cat, ID=7, Name=Sonya
    Cat, ID=8, Name=Tom
  Colors:     Black, Other
Cat, ID=6
  Age:        4
  Leg count:  4
  Parent:     Cat, ID=9
  Children:   1
    Cat, ID=7
  Name:       Sima
  Friends:    2
    Cat, ID=7, Name=Sonya
    Cat, ID=8, Name=Tom
  Colors:     Gray
Bat, ID=10
  Age:        2
  Leg count:  2
  Parent:     Bat, ID=10
  Children:   1
    Bat, ID=10
  Info.Text:  This bat is quite clever.
  Info.Mate:  Bat, ID=10
End.

Pam's children with Age>3:
Query result:
Cat, ID=6
  Age:        4
  Leg count:  4
  Parent:     Cat, ID=9
  Children:   1
    Cat, ID=7
  Name:       Sima
  Friends:    2
    Cat, ID=7, Name=Sonya
    Cat, ID=8, Name=Tom
  Colors:     Gray
End.


Press Enter to close...

Source code (Animals.cs)

using System;

using System.IO;
using System.Text;
using System.Threading;
using System.Globalization;
using DataObjects.NET;
using DataObjects.NET.Attributes;
using DataObjects.NET.FullText;
using DataObjects.NET.RuntimeServices;
using DataObjects.NET.ObjectModel;


namespace Demo_Animals
{
  class Animals
  {
    [STAThread]
    static void Main(string[] args)
    {
      Console.WriteLine("DataObjects.Net: Animals demo\n");

      string connectionUrl = "";
      while (connectionUrl=="") {
        Console.Write("Select database server to use:\n" +
                      "1) Microsoft SQL Server\n" +
                      "2) Oracle\n" +
                      "3) Native Oracle\n" +
                      "4) SAP DB\n" +
                      "> ");
        string dbType = Console.ReadLine();
        switch (dbType) {
          case "1":
            Console.WriteLine("Selected: Microsoft SQL Server.");
            connectionUrl = "mssql://localhost/DataObjectsDotNetDemos";
            break;
          case "2":
            Console.WriteLine("Selected: Oracle.");
            connectionUrl = "oracle://admin:admin@localhost/Demos";
            break;
          case "3":
            Console.WriteLine("Selected: Native Oracle.");
            connectionUrl = "nativeoracle://admin:admin@localhost/Demos";
            break;
          case "4":
            Console.WriteLine("Selected: SAP DB.");
            connectionUrl = "sapdb://admin:admin@localhost/Demos";
            break;
          default:
            Console.WriteLine("Illegal selection.");
            break;
        }
      }

      Console.WriteLine("Reading product key...");
      string productKeyFile = @"..\..\..\..\ProductKey.txt"; 
      string productKey = "";
      if (File.Exists(productKeyFile))
        using (StreamReader sr = new StreamReader(productKeyFile)) {
          productKey = sr.ReadToEnd().Trim();
        }

      Console.WriteLine("");
      Console.WriteLine("Building domain...");
      Domain domain = new Domain(connectionUrl, productKey);
      Console.WriteLine("Connection URL: {0}",domain.ConnectionURL);
      Console.WriteLine("Driver:         {0}",domain.Driver.Info.Description);

      domain.RegisterCulture(
        new Culture("En","U.S. English", new CultureInfo("en-us",false)));
      domain.Cultures["En"].Default = true; 
      domain.RegisterTypes("Demo_Animals");
      #if DEBUG
        domain.DebugInfoOutputFolder = @"C:\Debug";
      #endif
      domain.Build(DomainUpdateMode.Recreate);

      // Now domain is operable. From this point domain can be shared
      // (used concurrently) between multiple threads.
      
      using (Session session = new Session(domain)) {
        session.BeginTransaction();
        
        Cat simaCat    = (Cat)session.CreateObject(typeof(Cat));
        simaCat.Name   = "Sima";
        simaCat.Age    = 4;
        simaCat.Colors = Colors.Gray;

        Cat sonyaCat   = (Cat)session.CreateObject(typeof(Cat));
        sonyaCat.Name  = "Sonya";
        sonyaCat.Age   = 2;
        sonyaCat.Colors= Colors.Black | Colors.White;
        
        Cat tomCat     = (Cat)session.CreateObject(typeof(Cat));
        tomCat.Name    = "Tom";
        tomCat.Age     = 3;
        tomCat.Colors  = Colors.Black | Colors.Other;
        
        Cat pamCat     = (Cat)session.CreateObject(typeof(Cat));
        pamCat.Name    = "Pam";
        pamCat.Colors  = Colors.Other;
        
        // Using collections
        
        simaCat.Children.Add(sonyaCat); 
        // Note that sonyaCat.Parent now equals to simaCat
        
        simaCat.Parent = pamCat;
        tomCat.Parent  = pamCat;
        // Ok, we established a hierarchy now :)
        
        simaCat.Friends.Add(sonyaCat);
        // sonyaCat.Friends[0] now equals to simaCat

        tomCat.Friends.Add(sonyaCat);
        tomCat.Friends.Add(simaCat);
        // tomCat is friend of all cats now
        tomCat.Friends.Add(tomCat);
        // tomCat is friend even of itself :)

        Dump(session);
        
        // Let's play with Savepoint
        Savepoint sp = new Savepoint(session);

        pamCat.Remove(); 
        // Children property is [Contained] so all instances
        // are removed now
        
        Dump(session); // Heh, nothing
        
        sp.Rollback();
        
        Dump(session); // Cats are resurrected :)
        
        // Now let's add a Bat
        Bat someBat    = (Bat)session.CreateObject(typeof(Bat));
        someBat.Age    = 2;
        someBat.Children.Add(someBat); 
        // Well, ^ is not a normal situation, but we have only
        // one Bat for our experiments :)

        someBat.Info = new BatInfo();
        // DataObjects.Net persists bat instance 
        someBat.Info.Text = "This bat is quite clever.";
        // Note that DataObjects.Net persists Bat instance again because
        // BatInfo implements IDataObjectField. If you'll try to 
        // remove the support of this interface, nothing additional
        // will happen during execution of above line. In this 
        // case you can make the following trick to make
        // DataObjects.Net persist the instance:
        //   someBat.Info = someBat.Info;
        someBat.Info.Mate = someBat; // Coz there is no other Bats :)

        // And query the storage at last
        Query q = new Query(session, 
          "Select Animal instances where {LegCount}=2");
        Console.WriteLine("2-legged Animals:");
        Dump(q.Execute());

        // One more query
        q.Text = "Select Animal instances where {Age}>@MinAge "+
                 "order by {Age} desc, {ID}";
        q.Parameters.Add("@MinAge",2);
        Console.WriteLine("Animals with Age>2:");
        Dump(q.Execute());

        // And a difficult one (not well-optimized, but anyway...)
        q.Parameters.Clear();
        q.Text = "Select Animal instances "+
                 "where {Children[{Children.count}>0].count}>0";
        Console.WriteLine("Grandparents:");
        Dump(q.Execute());

        // Full-text search available only for MS SQL
        if (!domain.ExtractedDatabaseModel.ExtractedInfo.FullTextIndexingRunning)
          Console.WriteLine(
            "Full-text search\\indexing isn't available, skipping...\n");
        else {
          // Let's update full-text data immediately!
          FtIndexer ftIndexer = (FtIndexer)session.CreateService(typeof(FtIndexer), 1000000);
          ftIndexer.Execute();
          // Here we give a chance for the indexing service
          // to index our instances.
          session.Commit();
          Console.Write("Waiting for full-text index population.");
          for (int i = 0; i<20; i++) 
          {                            // You can increase constant 20 
            Thread.Sleep(1000);        // if the next query returns
            Console.Write(".");        // nothing
          }
          Console.WriteLine("");
          session.BeginTransaction();

          // Combined query (full-text search condition and ordinady condition)
          q.Text = 
            "Select Animal instances " +
              "where {Age}>2 or {LegCount}=2 " +
              "textsearch freetext 'Is Sima a Cat or a Bat?' " +
              "order by {FullTextRank} desc";
          Dump(q.Execute());
        }

        // Queriyng collection
        q = pamCat.Children.CreateQuery(
          "Select Animal instances where {Age}>3 order by {Age} desc");
        Console.WriteLine("Pam's children with Age>3:");
        Dump(q.Execute());

        session.Commit();
      }

      Console.Write("\nPress Enter to close... ");
      Console.ReadLine();
    }
    
    // This method dumps storage content
    static void Dump(Session session) 
    {
      Console.WriteLine("Storage content:");
      foreach (DataObject o in session.CreateQuery(
                               "Select DataObject instances").Execute()) {
        IDumpable dpo = o as IDumpable;
        if (dpo!=null)
          dpo.Dump(Console.Out);
        else
          Console.WriteLine("{0}, ID={1}", o.GetType().BaseType.Name, o.ID);
      }
      Console.WriteLine("End.\n");
    }

    // And this method can dump the result of a query
    static void Dump(QueryResult result) 
    {
      Console.WriteLine("Query result:");
      foreach (DataObject o in result) {
        IDumpable dpo = o as IDumpable;
        if (dpo!=null)
          dpo.Dump(Console.Out);
        else
          Console.WriteLine("{0}, ID={1}", o.GetType().BaseType.Name, o.ID);
      }
      Console.WriteLine("End.\n");
    }
  }

  
  // So usefull IDumpable interface
  public interface IDumpable {
    void Dump(TextWriter output);
  }
  
  // A base class for all animals
  public abstract class Animal: FtObject, IDumpable {
    [Indexed]
    [Nullable]
    public abstract int Age {get; set;}
    
    [Indexed]
    [Nullable]
    public abstract int LegCount {get; set;}
    
    // Referance
    [Indexed]
    [SelfReferenceAllowed]
    public abstract Animal Parent {get; set;}
    
    // Exciting thing - "reflected" collection
    [Contained] // So children will be removed on parent deletion
    [ItemType(typeof(Animal))]
    [PairTo(typeof(Animal),"Parent")] // "Reflects" Animal.Parent
    [SelfReferenceAllowed]
    public abstract DataObjectCollection Children {get;}
    
    protected override void OnSetProperty(string name, 
      Culture culture, object value)
    {
      if (name=="Parent") {
        if (value!=null)
          if (value.GetType()!=this.GetType())
            throw new InvalidOperationException(
              "Parent should be of the same type.");
      }
    }
    
    public virtual void Dump(TextWriter output)
    {
      // GetType().Name will print the name of the proxy class,
      // so we'll use GetType().BaseType.Name :)
      output.WriteLine("{0}, ID={1}", GetType().BaseType.Name, ID);
      output.WriteLine("  Age:        {0}", 
        GetProperty("Age")==null ? "Unknown" : Age.ToString());
      output.WriteLine("  Leg count:  {0}", 
        GetProperty("LegCount")==null ? "Unknown" : LegCount.ToString());
      if (Parent!=null)
        output.WriteLine("  Parent:     {0}, ID={1}", 
          Parent.GetType().BaseType.Name, Parent.ID);
      if (Children.Count!=0) {
        output.WriteLine("  Children:   {0}", Children.Count);
        foreach (Animal a in Children)
          output.WriteLine("    {0}, ID={1}",a.GetType().BaseType.Name, a.ID);
      }
    }

    // This method uses Dump method to populate fulltext index data
    public override string ProduceFtData(Culture culture) {
      // This will prevent recursive calls to the Persist method
      // on attempt to get an ID property value in the Dump method
      if (PersistDepth>1)
        return ""; 
      Culture oldCulture = this.Session.Culture;
      this.Session.Culture = culture;
      try {
        StringWriter sw = new StringWriter();
        Dump(sw);
        return sw.ToString();
      }
      finally {
        this.Session.Culture = oldCulture;
      }
    }
  }

  // A base class for all home animals
  public abstract class HomeAnimal: Animal {
    [Indexed]
    [SqlType(SqlType.Char)]
    [Length(32)]
    public abstract string Name {get; set;}

    [ItemType(typeof(HomeAnimal))]
    [Symmetric] // If A is friend of B then B is friend of A ?
    [SelfReferenceAllowed] // So A can be a friend of A 
    public abstract DataObjectCollection Friends {get;}

    public override void Dump(TextWriter output)
    {
      base.Dump(output);
      output.WriteLine("  Name:       {0}", Name);
      if (Friends.Count!=0) {
        output.WriteLine("  Friends:    {0}", Friends.Count);
        foreach (HomeAnimal a in Friends)
          output.WriteLine("    {0}, ID={1}, Name={2}", 
            a.GetType().BaseType.Name, a.ID, a.Name);
      }
    }
  }

  public abstract class Cat: HomeAnimal {
    [Indexed]
    public abstract Colors Colors {get; set;}
    
    protected override void OnCreate()
    {
      LegCount = 4;
    }

    public override void Dump(TextWriter output)
    {
      base.Dump(output);
      output.WriteLine("  Colors:     {0}", Colors);
    }
  }

  public abstract class Bat: Animal {
    // Custom-typed property
    public abstract BatInfo Info {get; set;}
    
    protected override void OnCreate()
    {
      LegCount = 2;
    }

    public override void Dump(TextWriter output)
    {
      base.Dump(output);
      if (Info==null) return;
      output.WriteLine("  Info.Text:  {0}", Info.Text);
      if (Info.Mate!=null)
        output.WriteLine("  Info.Mate:  {0}, ID={1}", 
          Info.Mate.GetType().BaseType.Name, Info.Mate.ID);
    }
  }

  [Flags] 
  public enum Colors {Black = 1, White = 2, Gray = 4, Other = 8}

  // Example of a custom property type
  // Rather curious :(
  [Serializable]
  public class BatInfo: IDataObjectField
  {
    // IDataObjectField support - it's not absolutely necessary
    // to write this support code, you can use simple
    // [Serializable] BatInfo, but in this case DataObjects.Net wouldn't
    // be notified when some property of this class will be changed.
    // Another good way is to use structs except classes for
    // such a properties - it's impossible to change the fields
    // of boxed structure using C# or VB.NET.
    [NonSerialized] DataObject holder;
    [NonSerialized] Field      field;
    [NonSerialized] Culture    culture;
    [NonSerialized] bool       attached;
    DataObject IDataObjectField.Holder  {get {return holder;}}
    Field      IDataObjectField.Field   {get {return field;}}
    Culture    IDataObjectField.Culture {get {return culture;}}
    
    void IDataObjectField.Attach(DataObject holder, Field field, 
      Culture culture)
    {
      if (attached)
        throw new InvalidOperationException(
          "Instance is already attached.");
      attached = true;
      this.holder  = holder;
      this.field   = field;
      this.culture = culture;
    }
  
    void IDataObjectField.Detach()
    {
      if (!attached)
        throw new InvalidOperationException(
          "Instance isn't attached.");
      attached = false;
    }
    
    // Simple string property
    string text;
    public string Text {
      get {return text;}
      set {
        text = value;
        // It's not necessary to write a code like this,
        // but if you want to provide automatic
        // persistence on the changes in the contained
        // fields\properties of this class, you should.
        holder.FieldContentChanged(this);
        // ^ this is how we notify DataObjects.Net that 
        // internal content of the property was changed,
        // so it should be persisted.
      }
    }
    
    // Reference to other bat - look how
    // DataObjects.Net will handle it.
    Bat mate;
    public Bat Mate {
      get {return mate;}
      set {
        mate = value;
        holder.FieldContentChanged(this);
      }
    }
  }
}