Browse by Tags

All Tags » database   (RSS)

In early 2010 I was working on several Silverlight projects that were driven by the need to provide a working offline solution. Isolated storage was the obvious answer. Unfortunately, serializing 10,000 contacts came with some overhead and I realized quickly that I was following some similar patterns when it came to handling large sets of data: I would serialize the key fields (for example, names for contacts) in order to manage lists that I could filter in-memory, while saving the bulk of data for separate serialization events.

This inspired me to create Sterling as a way to ease the burden of persisting data. I looked at other options and didn't like having to change my classes just to save data (for example, make them derive from a database base class when I already was deriving them from my own helper classes). I also didn't like the inflexibility of options available. So, I set out to build something lightweight and non-intrusive that would give the user full control over serialization. The key would be to provide keys and indexes through lambda expressions to make it easy to access common data using LINQ to Object queries in memory while saving the "slow" part (loading from disk) for when it was needed.

I knew I would use Sterling in my own projects but I never imagined how popular it would become with other users. The timing for Sterling was perfect because when the Windows Phone 7 was released, the application developers were faced with a unique scenario called "tombstoning" that requires saving data when the application is swapped to the background. Sterling turns out to be a perfect fit for saving and restoring that data.

After asking some of my peers whether or not it would make sense to provide the solution as an open source community project, the responses were consistent and I realized this was definitely the way to go. I uploaded the bits and quickly went from a fledgling "0.1" release to several interim change sets that morphed into something I was comfortable calling "beta."

That's when the real power of the open source community began to transform the project. Numerous people were pulling down Sterling and using it in their applications, then providing me with valuable feedback for features and bug fixes. Some even sent me code-complete patches to implement various changes. We went from a small set of options for serializing data to a rich, interactive, highly adaptive and flexible solution that can handle a large variety of class configurations and relationships.

It's been a challenging 8 months of late nights fixing bugs, responding to feature requests, and writing a comprehensive guide to make sure the product is well documented, but I would be lying if I said I didn't enjoy every moment. I'm excited to see what the future versions will bring but for now am absolutely ecstatic to announce that I've released Sterling from beta to version 1.0. Enough people have shared the testimonial of their projects and helped me expand the battery of unit tests on both the phone and browser versions to finally make this milestone possible.

You can download the release at CodePlex. You can read the full user's guide at www.SterlingDatabase.com. Please take the time to rate your experience with the product so others know what to expect, and if you are interested in what others are doing or are willing to share your own story, visit our Real World Sterling Projects thread. Thanks everyone and I look forward to exciting features to come.

Jeremy Likness

Sterling changeset 72063 [browse the source code here] introduces triggers. The trigger is a powerful feature that allows you to universally intercept database operations. Their application in Sterling resolves several concerns, including:

  • Validation — prevent a save operation from succeeding if data integrity is compromised, or prevent a delete operation when prerequisites are not met
  • Data-specific concerns — should the business layer be concerned with a "last modified date" that relates to the database? Use a trigger to set the date consistently without involving other pieces of your application
  • Post-save processing (for example, clear a "dirty flag" once the item is persisted)

Declaring the Trigger

Declaring the trigger in Sterling is straightforward. Every entity that is persisted by sterling is defined by a combination of the type and the key type. The trigger is no different. Internal to Sterling, a basic interface allows the database engine to manage triggers without having to close the generic type:

internal interface ISterlingTrigger
{
    bool BeforeSave(Type type, object instance);
    void AfterSave(Type type, object instance);
    bool BeforeDelete(Type type, object key);
}

Next, we want a more strongly typed version for definitions and for external sources that are aware of the type. This allows those interfaces to close the generic and work with a strongly typed interface:

internal interface ISterlingTrigger<T, TKey> : ISterlingTrigger where T: class, new() 
{
    bool BeforeSave(T instance);
    void AfterSave(T instance);
    bool BeforeDelete(TKey key);
}

Side Note: Covariance and Contravariance

Wikipedia has a good article explaining covariance and contravariance. For their application in C#, read the related Microsoft article. In C# you can declare types in generics as co- and contra- variant using the "in" and "out" keywords. The original interface definition used this until I realized they are no-go on the phone. Basically, the IDE and compiler will allow you to define them, but at runtime they fail with a type load exception.

Defining a Trigger

You may have noticed that the interfaces are internal to Sterling. That's because the way to define a trigger is through the base class that looks like this:

public abstract class BaseSterlingTrigger<T,TKey> : ISterlingTrigger<T,TKey> where T: class, new()
{
    public bool BeforeSave(Type type, object instance)
    {
        return BeforeSave((T) instance);
    }

    public void AfterSave(Type type, object instance)
    {
        AfterSave((T) instance);
    }
       
    public bool BeforeDelete(Type type, object key)
    {
        return BeforeDelete((TKey) key);
    }

    public abstract bool BeforeSave(T instance);

    public abstract void AfterSave(T instance);

    public abstract bool BeforeDelete(TKey key);
        
}

This is a common pattern I use when working with generics. The internal engine wants to deal with the object and the type, while the externals want to close the generic. In order to provide a contract to deal with a typed entity and keep the developer from worrying about any conversion, I can use the abstract class to overload from the non-typed to the typed version. The casting is very inexpensive compared to the reflection that would have to happen to manually invoke methods and close the generics in the core database engine.

The TriggerTest included with the Sterling project demonstrates the definition of a trigger:

public class TriggerClassTestTrigger : BaseSterlingTrigger<TriggerClass, int>
{
    public const int BADSAVE = 5;
    public const int BADDELETE = 99;

    private int _nextKey;
        
    public TriggerClassTestTrigger(int nextKey)
    {
        _nextKey = nextKey;
    }

    public override bool BeforeSave(TriggerClass instance)
    {
        if (instance.Id == BADSAVE) return false;
            
        if (instance.Id > 0) return true;

        instance.Id = _nextKey++;                       
        return true;
    }

    public override void AfterSave(TriggerClass instance)
    {
        instance.IsDirty = false;
    }

    public override bool BeforeDelete(int key)
    {
        return key != BADDELETE;
    }
}

This trigger does a few things. It takes in a key and stores that value. In the case of integer identity fields, for example, the portion of your code that initializes the database can perform a query to find the maximum key that exists. You can then increment the value and pass it to the trigger class, which will auto-set the identity for new entities (assuming anything without a positive non-zero id is new) and keep track of the next key.

There is an arbitrary validation that returns false if the id is an explicit value. The Sterling database engine will throw a SterlingTriggerException if the BeforeSave or BeforeDelete methods return false, preventing the class from being persisted. This is meant as a last resort, as exceptions are expensive and your code should validate these conditions and prevent them prior to saving.

In the AfterSave you can see the example of automatically clearing the dirty flag.

Registering the Trigger

Registering the trigger is straightforward. Triggers can be registered anytime after the database is activated (this allows you to query the database and preset and trigger conditions prior to activating them). They can also be unregistered. Triggers in Sterling are more like inceptors and may be transient, rather than traditional relational database triggers which can be thought of as part of the table definition itself.

The following code demonstrates a pattern for handling auto-identity fields. It activates the Sterling engine, registers and activates the database, queries for the highest key value and then registers the trigger using the last known key:

_engine = new SterlingEngine();
_engine.Activate();
_databaseInstance = _engine.SterlingDatabase.RegisterDatabase<TriggerDatabase>();

var nextKey = _databaseInstance.Query<TriggerClass, int>().Any() ?
    (from keys in _databaseInstance.Query<TriggerClass, int>()
        select keys.Key).Max() + 1 : 1;

_databaseInstance.RegisterTrigger(new TriggerClassTestTrigger(nextKey));

Afterword: Windows Phone 7 Tests

As of this blog date an interesting issue exists that the Sterling team is investigating. Triggers appear to work perfectly fine on the Windows Phone 7 and the sample project has been updated to use a trigger. However, when the trigger is included in the unit tests for the phone, the unit test harness initializes but fails to run any tests. This is true when simply the definition of the trigger class is provided.

I will follow up once we determine the cause. It appears to be an issue with the unit test framework and I suspect something happens when it scans the types in the assembly to find tests. For now we have the unit test disabled on the phone but will update you once the cause is found and rectified.

Sterling is getting closer to versoin 1.0 RTM. The remaining pieces include a similar trigger-like architecture for intercepting the byte streams to enable encryption and compression or other manipulation, and streams to expose a backup and restore mechanism.

Visit Sterling online at http://sterling.codeplex.com/.

Jeremy Likness

Sterling is an object-oriented database that I created to facilitate ease of serialization of data in both Silverlight 4 (soon 5) and Windows Phone 7 projects. Sterling was designed with a few goals in mind. It is not a true relational database and not intended to replace one. I'm confident certain offerings such as SQLite or even a trimmed down version of SQL Server will make their way to the phone eventually, backed by experienced teams and highly optimized code.

Instead, I set out with a few fundamental goals:

  1. Keep it as lightweight as possible — no one wants to add a huge DLL just to facilitate some functionality
  2. Keep it extensible and flexible to address as many needs as possible
  3. Take advantage of LINQ to Objects to provide keys and indexes for ultra-fast querying of data and lazy-deserialization from isolated storage
  4. Keep it non-intrusive — users should not have to modify their existing types to use the database system, such as inheriting from a base class

I believe I've successfully addressed those needs and Sterling is quickly being adopted in major projects. The community has contributed some excellent suggestions and even modifications to allow for a very rich feature set. Sterling currently supports:

  • Out-of-the-box support for most native value types
  • Automatic enum support
  • Tuple and Lazy support on the phone
  • Support for Nullable<T>
  • Handles ICollection, IList, and IDictionary
  • Supports base classes (will serialize/deserialize the derived types)
  • Provides keys and indexes (covered indexes means in-memory querying)
  • Lazy loading of serialized values
  • Multiple database support (for versioning or partitioning of data in complex applications)
  • Custom serialization for types not supported out of the box
  • Custom logging of database events
  • In spite of all of these features, Sterling still holds it own for speed and because it uses a binary format, is quite compact on disk

This post is intended to introduce you to Sterling and also to provide guidance for using it on the Windows Phone 7. I am not supplying a full project as this is being built for the full 1.0 release, but I've received numerous requests for clarification and guidance on the phone, so this post is intended to fill that gap until the full Sterling documentation is released.

Download Sterling

Your first step will be to download Sterling. As of this post, you'll want to grab the latest source as there have been numerous bug fixes and optimizations that were not in the latest formal release. These will be integrated into the 1.0 RTM which is slated for early 2011. You can visit this link and choose "Download" in the box to the upper right that indicates "Latest Version."

Build Sterling

Navigate to the SterlingSln directory and choose the WindowsPhoneSterlingSln.sln file to open the source code for Windows Phone 7. It is up to you whether you want to build the project and include Wintellect.Sterling.WindowsPhone.dll in your project, or simply use a project reference. As of this blog post, the DLL weighs in at a light 72 kilobytes on disk.

Sterling Under the Covers

Before you wire Sterling into your Windows Phone 7 application, it helps to understand how it works under the covers. Below is a diagram of the rough directory structure that Sterling uses to serialize data:

Sterling Folder Structure

Sterling creates an aptly named Sterling folder at the root of isolated storage. It contains a file called db.data which simply maps database names to folders that are named sequentially. This shortens the folder length but also helps to avoid collisions with type names in the folder structure and/or name.

The first database receives a subfolder named 0. The root of that folder contains a tables.dat file that maps type names for classes that Sterling is using to another set of subfolders, one for each type. The first class to be referenced for the database will receive a subfolder 0 as well.

To put things in perspective, if you have a database named "Contacts" and a type called "ContactEntity" then the path to the data for the ContactEntity is Sterling\0\0 where 0 represents the first database and 0 represents the first type. A second database will reside at Sterling\1 and a second type in the first database will reside at Sterling\0\1.

Within the type folder, there are several files. There is always a keys.dat file that maps key values to ordinals. For example, if you have a key that is a GUID, this file will map the GUID value to "0" for the first record, "1" for the second record, etc. Each row in the table is stored as a separate file for fast access. If you defined any indexes for the table, the indexes are mapped to keys in a file named {indexname}.dat.

Sterling serializes the rows as soon as they are saved. For efficiency, Sterling does not flush the keys each time. Imagine saving 1,000 records. If Sterling serialized the keys each time, it would have to serialize a 1 row record, then overwrite that with a 2 row record, etc. until the 1,000 rows were written. Instead, Sterling will wait until you call the Flush method before writing the keys and indexes to disk.

As a best practice, you should flush after serializing types. If you are saving or updating a single type, flush after the save. If you are performing a bulk operation, wait until after all of the types are serialized and then call flush. Doing so ensures the integrity of the database. Sterling tracks a "dirty flag" on the keys and indexes, and when the application tombstones, it will flush any keys or indexes that haven't already been saved. You will speed the tombstone process if the keys/indexes have already been flushed. No flush is required after queries or other read-only operations.

Creating a Sterling Database

Once you are ready to begin using Sterling, you will need to define a database. Typically each application will have a single database that supports multiple types. Sterling provides the facility to use multiple databases for ease of versioning and for data partitioning. A database is simply a collection of types that will be persisted.

A Sterling database inherits from the BaseDatabaseInstance. There are two required overrides. The first defines the name of the database and should be a unique name within your application. The second defines what types the database will support. To define a type, simply use the base CreateTableDefinition. You must supply the type that will be serialized, along with the type of the unique key. Keys can be any value supported by Sterling. Keys that are not directly supported, or are based on complex classes, must have a serializer defined (this will be discussed later). For now, assume a simple integer key. The base method takes one parameter, and that is a lambda expression that instructs Sterling how to get the key value for a given type.

Here is an item view model, taken and modified from the default Windows Phone 7 project:

public class ItemViewModel : INotifyPropertyChanged
{
    private int _id;

    public int Id
    {
        get { return _id; }
        set
        {
            if (value != _id)
            {
                _id = value;
                NotifyPropertyChanged("Id");
            }
        }
    }

    private string _lineOne;
    public string LineOne
    {
        get
        {
            return _lineOne;
        }
        set
        {
            if (value != _lineOne)
            {
                _lineOne = value;
                NotifyPropertyChanged("LineOne");
            }
        }
    }

    private string _lineTwo;
    public string LineTwo
    {
        get
        {
            return _lineTwo;
        }
        set
        {
            if (value != _lineTwo)
            {
                _lineTwo = value;
                NotifyPropertyChanged("LineTwo");
            }
        }
    }

    private string _lineThree;
    public string LineThree
    {
        get
        {
            return _lineThree;
        }
        set
        {
            if (value != _lineThree)
            {
                _lineThree = value;
                NotifyPropertyChanged("LineThree");
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void NotifyPropertyChanged(String propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (null != handler)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

To notify Sterling about this type is as simple as the following code, which defines a database to store the item:

public class ItemDatabase : BaseDatabaseInstance 
{

    public override string Name
    {
        get { return "ItemDatabase"; }
    }

    protected override System.Collections.Generic.List<ITableDefinition> _RegisterTables()
    {
        return new System.Collections.Generic.List<ITableDefinition>
        {
            CreateTableDefinition<ItemViewModel,int>(i=>i.Id)
        };
    }
}

Supporting other types is as simple as adding a comma after the CreateTableDefinition call and using the same method to register another type.

Adding Indexes

Sterling allows the definition of indexes as well. Indexes are stored in memory, so you must take care when deciding what indexes will be used - the trade-off is memory vs. the speed of serialization/de-serialization. Indexes are intended to provide fast in-memory lookup of data. Once the data is filtered and selected, the full type can be de-serialized from disk. This lazy loading ensures that queries and look ups are fast and efficient.

To add an index to a type registration, use the WithIndex extension method. Indexes always include the key. The format for an index is {classType,indexType,keyType}. Sterling supports indexes with one or two indexed properties (technically, anything you can access and serialize with a lambda expression is available as an index). Here is an example index using the LineOne property of the ItemViewModel. It can be referred to anywhere in code using the "LineOneIndex" name:

return new System.Collections.Generic.List<ITableDefinition>
{
    CreateTableDefinition<ItemViewModel,int>(i=>i.Id)
    .WithIndex<ItemViewModel,string,int>("LineOneIndex",i=>i.LineOne)
};

Setting up Sterling for Tombstoning

Now that you've defined a database, some types and an index, it's time to integrate the database with your Windows Phone 7 application. The majority of integration will happen in the App.xaml.cs to allow for hooking into the phone tombstone events.

First, add references to the top of the class for the components that are used by the Sterling database engine:

private static ISterlingDatabaseInstance _database = null;
private static SterlingEngine _engine = null;
private static SterlingDefaultLogger _logger = null;

It is also suggested you expose a static property to make it easy to reference the database from anywhere within your application:

public static ISterlingDatabaseInstance Database
{
    get
    {
        return _database;
    }
}

Next, create two methods. The first is designed to activate the database when the application is first launched, or when the phone wakes up from a tombstone event. The second will deactivate the engine when the application is exited or tombstoned.

private void _ActivateEngine()
{
    _engine = new SterlingEngine();
    _logger = new SterlingDefaultLogger(SterlingLogLevel.Information);
    _engine.Activate();
    _database = _engine.SterlingDatabase.RegisterDatabase<ItemDatabase>();
}

private void _DeactivateEngine()
{
    _logger.Detach();
    _engine.Dispose();
    _database = null;
    _engine = null;
}

Notice the log level. This example uses the built-in Sterling logger, which simply dumps output to the debug window. Refer to the documentation for instructions on how to write our own logger and register it with the Sterling engine.

Now you can hook these events into the App.xaml.cs code behind using the phone lifecycle events:

private void Application_Launching(object sender, LaunchingEventArgs e)
{
    _ActivateEngine();
}

private void Application_Activated(object sender, ActivatedEventArgs e)
{
    _ActivateEngine();

    if (!App.ViewModel.IsDataLoaded)
    {
        App.ViewModel.LoadData();
    }
}

private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
    _DeactivateEngine();
}

private void Application_Closing(object sender, ClosingEventArgs e)
{
    _DeactivateEngine();
}
That's all that is required to have the engine ready and waiting for your application.

Example Sterling Operations

Saving Objects

Saving objects in Sterling is straightforward. As long as the type has been defined, you simply call the Save method on the database instance. New records will be automatically inserted, and existing records will be automatically updated. In this example, sampleData contains a list of ItemViewModel. The code snippet iterates the list, setting a unique identifier, and saves them. Note the flush for the keys that is called after the individual items are saved.

foreach (var item in sampleData)
{
    idx++;
    item.Id = idx;
    App.Database.Save(item);                
}   
         
App.Database.Flush();

Direct Loading

To directly load an instance, simply pass the type and the key. For example, this code will load the item with an id of 2:

var itemViewModel = App.Database.Load<ItemViewModel>(2);

Querying Keys

The phone does not directly support the IQueryable interface. Keys on the phone are stored in memory and exposed via an IEnumerable interface. To avoid corruption of the keys, you cannot directly manipulate the keys. For filtering and sorting, you should first cast the keys to a list, like this:

var keyList = from key in App.Database.Query<ItemViewModel, int>().ToList() 
                where key.Key > 5 
                select key;

Note the syntax: the query method is passed the type of the class and the type of key. After casting to a list, the resulting dataset can be queried, sequenced, or any other operation performed that is supported by LINQ to Objects on the Windows Phone 7.

The key automatically provides a lazy-loaded reference to the entire class. The following query is similar to the previous example, but will lazily de-serialize the actual class instance instead of supplying a list of keys:

var instanceList = from key in App.Database.Query<ItemViewModel, int>().ToList()
                    where key.Key > 5
                    select key.LazyValue.Value; 

Using Indexes

Using indexes is very similar to using keys. You must specify the type of the index as well as the name of the index. In this example, the index is used to provide a list of identifiers for the item view models, sorted in the order of the text in the LineOne property:

var sortedByLineOne = from index in App.Database.Query<ItemViewModel, string, int>("LineOneIndex").ToList()
                                  orderby index.Index
                                  select index.Key;

Complex Queries

Sterling supports any type of query that is supported by LINQ to Objects. In the online documentation, there is an example query that joins two different indexes and creates a new anonymous type with the merged values - this is how it would look on the phone:

return from n in CurrentFoodDescription.Nutrients
        join nd in
            SterlingService.Current.Database.Query<NutrientDefinition, string, string, int>(
                FoodDatabase.NUTR_DEFINITION_UNITS_DESC).ToList()
            on n.NutrientDefinitionId equals nd.Key
        join nd2 in
            SterlingService.Current.Database.Query<NutrientDefinition, int, int>(
                FoodDatabase.NUTR_DEFINITION_SORT).ToList()
            on nd.Key equals nd2.Key
        orderby nd2.Index
        select new NutrientDescription
                    {
                        Amount = n.AmountPerHundredGrams,
                        Description = nd.Index.Item2,
                        UnitOfMeasure = nd.Index.Item1
                    };

Custom Serialization with Sterling

The last thing to cover with Sterling is custom serialization. Sterling has been updated to support as many types "out of the box" as possible. Support for null values, enums, base types, etc. has been baked in. Version 1.0 will also support complex object graphs as long as the nested properties are able to be serialized. Inevitably, whether because you need to manipulate the data or because you have a specific type not supported, you may need to provide a custom serializer. With Sterling this requires only two steps.

The first step is to inherit from the BaseSerializer class. One custom serializer can handle multiple types, and must implement a method to indicate which types are supported. In this example, the serializer supports a custom struct:

public class FoodSerializer : BaseSerializer  
{
    public override bool CanSerialize(Type targetType)
    {
        return targetType.Equals(typeof (NutrientDataElement));                
    }

    public override void Serialize(object target, BinaryWriter writer)
    {
        var data = (NutrientDataElement)target;
        writer.Write(data.NutrientDefinitionId);
        writer.Write(data.AmountPerHundredGrams);
    }

    public override object Deserialize(Type type, BinaryReader reader)
    {
        return new NutrientDataElement
                    {
                        NutrientDefinitionId = reader.ReadInt32(),
                        AmountPerHundredGrams = reader.ReadDouble()
                    };
    }
}

Note that the serializer indicates what type it supports, then provides the steps for serializing and de-serializing those types.

The second step is to simply register the custom serializer with the database before it is activated. That step looks like this:

_engine.SterlingDatabase.RegisterSerializer<FoodSerializer>();
_engine.Activate();

Conclusion

As you can see, Sterling is flexible, lightweight, and easy to use. The community continues to drive new features and version 1.0 will release in early 2011. Consider using Sterling to help with tombstoning and persistence of data in your upcoming Windows Phone 7 project. If you have any questions, don't hesitate to ask in our forums (that's a great place to post your success stories as well) and be sure to log any feature requests in our Issue Tracker database. Thanks!

Jeremy Likness

Today, I'm excited to share a project that I've been working on for some time now. The project, called Sterling (for Sterling Silverlight, of course!) is a very light weight approach to handling serialization and deserialization to and from isolated storage. I call it an object-oriented database because it provides LINQ to Object queries over keys and indexes.

I guess the timing works well ... I was recently honored with the 2010 Microsoft Most Valuable Professional (MVP) award for my work with Silverlight. Much of this award is about what we do for the community, and this is one of my first major efforts to put a utility and open source project out there. Lots of hours went into this but I hope there will be great benefits for those of you who find value in using it.

Sterling is not yet released and I don't anticipate we'll have an alpha ready until late July. I work on many projects for my company, so this effort has been primarily late nights or snatches during lunch and breaks and will probably continue to be so. I'm excited that some members of the community have offered to reach out and help push development forward.

First, let me emphasize that Sterling is not intended as a replacement for a robust, transactional database system. There are some commercial and non-commercial solutions that are optimized for performance and massive scale. I wrote Sterling to be a simple serialization engine to avoid having to deal with the goop of writing my own serializers all of the time. I also know that even if you have a simple list of contacts, querying the contact names is going to be a priority over deserializing all of them, so I built a key and index infrastructure to facilitate storing key items in memory so you can bind to lists and combo-boxes without de-serializing the entire object.

Having said all of this, while a lot of more powerful features may not exist, my goal was to hit 80% of the needs with 20% of the footprint. Not only is Sterling incredibly lightweight, but extensible and available on Windows Phone 7.

Here are the key goals I had in mind:

Non-intrusive

I don't like mangling my classes to persist them. I didn't want to force anyone to inherit from a base class or decorate the class with attributes to make it work. I believe I've achieved this goal. Defining a "table" to sterling is as simple as passing a type and a lambda expression that returns the key:

public override List<ITableDefinition> _RegisterTables()
{
 return new List<ITableDefinition>
                       {
                           CreateTableDefinition<Contact,string>(c => c.Email)
                       }
}

I can even do this for sealed classes or third-party classes - whatever your code can see, Sterling can, too. Sterling automatically serializes the same values that the BinaryWriter class supports. If you need something custom, no problem - just define a serializer of your own and register it with Sterling.

Lightweight

I wanted Sterling to fit on the Windows Phone 7 and also to facilitate building projects without bloating them. I purposefully kept it lightweight. Currently the DLL weighs in at only 70 kilobytes, which I believe is very trivial compared to some solutions I've seen.

Flexible and Portable

Again, these are both features that collapse into the previous items. By keeping it flexible, I can accommodate needs I didn't know at design time. There is a very loose interface for logging and for extending serialization. This allows you to do pretty much whatever you like, without having to think about the underpinnings of setting up tables and folders and checking if they exist, etc. Portability means it was easy to build on Windows Phone 7 and should be very easy to bring forward into future versions of Silverlight.

The Reference Application

The easiest way to get to know Sterling is by the reference application. I will warn you that it takes a long time to build because of the time it takes to initially serialize. I decided a good test case would be the USRDA nutrient database. It contains over 500,000 data elements. I had to write some parsers to take the source text-based database files and turn them into data objects that Sterling could deal with, but once the conversion is done, you can see the power of how Sterling operates.

The left column shows a custom logger I made that is spitting out Sterling log information. The data models were straightforward. A food group is a general "category" for foods to fall under. The food description is an actual food item, and contains a list of nutrient data elements (things like calories, protein, vitamin A, etc). These point to a nutrient definition.

public class FoodGroup
{
    public int Id { get; set; }
    public string GroupName { get; set; }
}

public class NutrientDefinition
{
    public int Id { get; set; }

    public string UnitOfMeasure { get; set; }

    public string Tag { get; set; }

    public string Description { get; set; }

    public int SortOrder { get; set; }
}

public struct NutrientDataElement
{
    public int NutrientDefinitionId { get; set; }
    public double AmountPerHundredGrams { get; set; }
}

public class FoodDescription
{
    public FoodDescription()
    {
        Nutrients = new List();
    }

    public int Id { get; set; }

    public int FoodGroupId { get; set; }

    public string Description { get; set; }

    public string Abbreviated { get; set; }

    public string CommonName { get; set; }

    public string Manufacturer { get; set; }

    public string InedibleParts { get; set; }

    public double PctRefuse { get; set; }

    public string ScientificName { get; set; }

    public double NitrogenFactor { get; set; }

    public double ProteinCalories { get; set; }

    public double FatCalories { get; set; }

    public double CarbohydrateCalories { get; set; }

    public List&t;NutrientDataElement> Nutrients { get; set; }
}

Because I have a struct for the nutrient information, I had to provide a custom serializer:

public class FoodSerializer : BaseSerializer  
{
    public override bool CanSerialize(Type targetType)
    {
        return targetType.Equals(typeof (NutrientDataElement));                
    }

    public override void Serialize(object target, BinaryWriter writer)
    {
        var data = (NutrientDataElement)target;
        writer.Write(data.NutrientDefinitionId);
        writer.Write(data.AmountPerHundredGrams);
    }

    public override object Deserialize(Type type, BinaryReader reader)
    {
        return new NutrientDataElement
                    {
                        NutrientDefinitionId = reader.ReadInt32(),
                        AmountPerHundredGrams = reader.ReadDouble()
                    };
    }
}

As you can see, fast and easy to do. Defining the tables took a little bit of thought. I wanted a "covered index" for food groups so I wouldn't have to de-serialize them at all. For the food descriptions, I needed an index on description and food group for fast filtering and sorting. Finally, the nutrient definitions provided an index for unit of measure and sort order (so they sort consistently in each food item).

public class FoodDatabase : BaseDatabaseInstance
{
    public override string Name
    {
        get { return "Type Database"; }
    }

    public const string FOOD_GROUP_NAME = "FoodGroup_GroupName";
    public const string FOOD_DESCRIPTION_DESC_GROUP = "FoodDescription_Description_Group";
    public const string NUTR_DEFINITION_UNITS_DESC = "NutrientDefinition_Units_Description";
    public const string NUTR_DEFINITION_SORT = "NutrientDefinition_Sort";
        
    protected override List<ITableDefinition> _RegisterTables()
    {
        return new List<ITableDefinition>
                    {
                        CreateTableDefinition<FoodGroup, int>(fg => fg.Id)
                            .WithIndex<FoodGroup, string, int>(FOOD_GROUP_NAME, fg => fg.GroupName),
                        CreateTableDefinition<FoodDescription, int>(fd => fd.Id)
                            .WithIndex<FoodDescription, string, int, int>(FOOD_DESCRIPTION_DESC_GROUP,
                                                                            fd =>
                                                                            Tuple.Create(fd.Description, fd.FoodGroupId)),
                        CreateTableDefinition<NutrientDefinition,int>(nd=>nd.Id)
                            .WithIndex<NutrientDefinition,string,string,int>(NUTR_DEFINITION_UNITS_DESC,
                            nd=>Tuple.Create(nd.UnitOfMeasure,nd.Description))
                            .WithIndex<NutrientDefinition,int,int>(NUTR_DEFINITION_SORT,
                            nd=>nd.SortOrder)
                    };
    }
}

The main view model allows you to select groups and enter search terms, displays the food items, and then will show a chart breaking out nutrition information when you click on the food item. The food group list is queried using the index like this:

public IEnumerable<FoodGroup> FoodGroups
{
    get
    {
        return DesignerProperties.IsInDesignTool
                    ? _samples.AsEnumerable()
                    : from fg in
                            SterlingService.Current.Database.Query<FoodGroup, string, int>(
                                FoodDatabase.FOOD_GROUP_NAME)
                        select new FoodGroup {Id = fg.Key, GroupName = fg.Index};
    }
}

Note that I have a default list for design-time, otherwise I run the actual query.

When you hover over a food group, I supply a tool-tip to show the number of food items in that group. I decided to go ahead and store these in a dictionary after querying the first time, but only because I know they won't change. The converter looks like this:

private readonly Dictionary<int,int> _foodCounts = new Dictionary<int,int>();

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
    var count = DesignerProperties.IsInDesignTool ? 500 : 0;
            
    var foodGroup = value as FoodGroup;
    if (foodGroup != null && !DesignerProperties.IsInDesignTool)
    {
        if (_foodCounts.ContainsKey(foodGroup.Id))
        {
            count = _foodCounts[foodGroup.Id];
        }
        else
        {
            count =
                (from index in
                        SterlingService.Current.Database.Query<FoodDescription, string, int, int>(
                            FoodDatabase.FOOD_DESCRIPTION_DESC_GROUP)
                    where index.Index.Item2.Equals(foodGroup.Id)
                    select index).Count();
            _foodCounts.Add(foodGroup.Id,count);
        }
    }

    return string.Format("There are {0} food items in this food group.", count);
}

Here you can see I'm providing a design-time default. If I'm not in the designer, then I go ahead and calculate the amount and save it using the index I created. The index has two values (description and food group key) so I use a Tuple to access the value. None of this requires any de-serialization because I'm only touching the index.

Food group counts

The search view model uses the same index to pull food items. The query is a bit more complex. I'm not allowing you to search all 10,000 food items. You must narrow it to a category or enter at least three characters of a search (the search is a containing search, not a "starts with" or "ends with").

public IEnumerable<FoodDescriptionIndex> SearchResults
{
    get
    {
        if (DesignerProperties.IsInDesignTool)
            return _sampleDescriptions.AsEnumerable();

        if (_currentGroup != null)
        {
            if (string.IsNullOrEmpty(_searchText) || _searchText.Length (
                                        FoodDatabase.FOOD_DESCRIPTION_DESC_GROUP)
                                where
                                    fg.Index.Item2.Equals(_currentGroup.Id)
                                select
                                    new FoodDescriptionIndex {Id = fg.Key, Description = fg.Index.Item1};

                return query1.Count() == 0 ? _noResults.AsEnumerable() : query1;
            }

            // group and search text)
            var query2 = from fg in
                        SterlingService.Current.Database.Query
                        <FoodDescription, string, int, int>(
                            FoodDatabase.FOOD_DESCRIPTION_DESC_GROUP)
                    where
                        fg.Index.Item2.Equals(_currentGroup.Id) &&
                        fg.Index.Item1.ToUpperInvariant().Contains(_searchText.ToUpperInvariant())
                    select
                        new FoodDescriptionIndex {Id = fg.Key, Description = fg.Index.Item1};
                        
            return query2.Count() == 0 ? _noResults.AsEnumerable() : query2;
        }

        if (string.IsNullOrEmpty(_searchText) || _searchText.Length (
                        FoodDatabase.FOOD_DESCRIPTION_DESC_GROUP)
                where
                    fg.Index.Item1.ToUpperInvariant().Contains(_searchText.ToUpperInvariant())
                select
                    new FoodDescriptionIndex { Id = fg.Key, Description = fg.Index.Item1 };
        return query3.Count() == 0 ? _noResults.AsEnumerable() : query3; 
    }
}

Food items

Notice how I'm using a "covered index" (covered for the items I need, which include the key and description) to query and return a list of types that are bound to the list box. You can see when you run the example this happens very fast. Finally, when you click on the item, I am always deserializing rather than trying to cache a ton of objects.

The food description context class synchronizes the current food item between view models. Notice when it is passed a value, it loads the new item:

public class FoodDescriptionContext : BaseNotify 
{
    public static FoodDescriptionContext Current = new FoodDescriptionContext();

    public FoodDescription CurrentFoodDescription { get; private set; }

    private int _foodDescriptionId; 

    public int FoodDescriptionId
    {
        get { return _foodDescriptionId; }
        set
        {
            _foodDescriptionId = value;
            CurrentFoodDescription = SterlingService.Current.Database.Load<FoodDescription>(value);
            RaisePropertyChanged(()=>CurrentFoodDescription);
            RaisePropertyChanged(()=>FoodDescriptionId);
        }
    }

}

This isn't thread-safe, but does it need to be? The user can only click on one item at a time.

Food detail

Finally, the whole engine is configured using an application service.

There's a lot more to it but I wanted to get this out there and have people start looking at it to provide me with feedback. While there is not yet a release, you can visit the Sterling Codeplex site to download the code (pre-alpha, so use at your own risk, right?) and build/test/integrate on your own. Let me know what you like and don't like and if you are interested in being a serious beta tester, and we'll see what we can do to release a solid version 1.

(PS, I took a simple list project for the Windows Phone 7 just to prove the engine works there - I simply save the list to the database then bind to the query - but I'm looking for a more comprehensive example there, so volunteers are welcome to work on that as well!)

Jeremy Likness