Implementing an XML Data Provider for Oxite, Part II

June 13th, 2009 | Tags:

In my previous post, I tackled the concept of an XML Data Provider for Oxite and outlined one possible structure for storing the data.

What I have done so far, is write some generic Linq to XML classes that do most of the heavy lifting of storing and retrieving objects using reflection, and used those to implement the repositories.

I have then implemented a class called OxiteXmlContext which aims to resemble a traditional Linq to SQL data context. This class has properties like Tags, Sites, Languages, Phrases and so on. These “tables” wrap up all the functionality of storing, retrieving and removing objects in an XDocument (one for each table).

This has allowed me to reuse some of the query logic in the Linq to SQL provider. But in many cases I have been able to greatly simplify or remove the queries entirely. This, due to the fact that the XML provider does not have a second layer of classes on top of the native Oxite model, and thus not requiring any projection.

The larger storages, such as posts, pages, comments and so on, where it isn’t feasible to read everything into memory and sort it out afterwards, have been split into one folder per post/page. Each folder contains separate files for body, comments and trackbacks. This is to avoid having to rewrite the post body when a new comment is added. The relations between posts, pages, tags and areas have been indexed in separate files along with creation and publish dates, status and so on. These indexes might grow a bit once post count reaches a thousand or so. Hopefully if the author is diligent enough to post thousands of posts, the author in question will also be enough of an enthusiast to invest in an SQL storage instead.

As you can see by this class diagram, the hierarchy is quite straightforward.

xmlclasses If you look closely, you’ll see a property on the OxiteXmlContext called PostHeaders. This property is actually the index and translates to a class called PostHeader. PostHeader contains, as the name indicates, header fields for the post, such as Id, Title, Status, Publish date, Tags and so on. It also contains a method called GetPost, which fetches the post from disk by reading the body.xml, comments.xml and trackbacks.xml files in the post’s folder.

Saving of the post is done either in whole by calling the Save method on OxiteXmlContext or by saving individual parts of it by calling the SaveBody, SaveComments or SaveTrackbacks method.

Less complex types, such as Site and Plugin which also contain child objects, but don’t deserve having them stored in separate files like comments for posts, instead inherit from the XmlTable class and override the serialization methods to store/retrieve these sub items from the XDocument directly.

public class OxitePluginTable : XmlTable<Plugin>
{
    public OxitePluginTable(string baseDirectory)
        : base(baseDirectory, "ID")
    {
    }

    protected override XElement ProjectEntity(TEntity entity)
    {
        XElement element = base.ProjectEntity(entity);

        Plugin plugin = (Plugin)entity;

        element.Add(new XElement("settings",
            from r in plugin.Settings.AllKeys
            select new XElement(r, plugin.Settings[r])));

        return element;
    }

    protected override TEntity ProjectEntity(XElement element)
    {
        Plugin plugin = (Plugin)base.ProjectEntity(element);
        plugin.Settings = new NameValueCollection();

        element.Element("settings").Elements().ToList().ForEach(e =>
            plugin.Settings.Add(e.Name.ToString(), e.Value));

        return plugin;
    }
}

As you can see by the PluginRepository, the code is strikingly similar to that of Linq to SQL, yet not quite exactly the same.

public class PluginRepository : IPluginRepository
{
    private OxiteXmlContext mContext;

    public PluginRepository(Site site)
    {
        mContext = new OxiteXmlContext(site);
    }

    public IList<IPlugin> GetPlugins()
    {
        var query = from p in mContext.Plugins
                    orderby p.Category, p.Name
                    select p;

        return query.Cast<IPlugin>().ToList();
    }

    public IPlugin GetPlugin(Guid pluginID)
    {
        return mContext.Plugins.GetEntity(pluginID);
    }

    public bool GetPluginExists(Guid pluginID)
    {
        return mContext.Plugins.GetEntity(pluginID) != null;
    }

    public void Save(IPlugin plugin)
    {
        mContext.Plugins.Save(projectPlugin(plugin));
    }

    private Plugin projectPlugin(IPlugin p)
    {
        return new Plugin
        {
            Category = p.Category,
            Enabled = p.Enabled,
            ID = p.ID,
            Name = p.Name,
            Settings = p.Settings
        };
    }

    public void Save(IPlugin plugin, NameValueCollection settings)
    {
        Plugin p = projectPlugin(plugin);
        p.Settings = settings;
        mContext.Plugins.Save(p);
    }

    public NameValueCollection GetPluginSettings(Guid pluginID)
    {
        return GetPlugin(pluginID).Settings;
    }

    public void SaveSetting(Guid pluginID, string name, string value)
    {
        Plugin plugin = mContext.Plugins.GetEntity(pluginID);
        plugin.Settings[name] = value;
        mContext.Plugins.Save(plugin);
    }
}

What’s next?

IPageRepository

The page repository still remains to be implemented.

Caching

I aim to add a simple per-process in-memory cache of entities to avoid having to fetch them from disk each time they are requested. This not only helps performance each time a page is served, but also on a lower level, since the Linq queries can result in multiple hits on the same entity, which would result in the same entity being deserialized multiple times.

Tread-safety

None of the classes are currently thread-safe, which they need to be in order for Oxite to be able to serve more than one visitor at a time, lest two instances try and write to the same file at the same time! I have purposefully skipped this part is I will implement this as part of the caching mechanism and refactor the repositories to go through the cache which in turn will serialize requests into the store.

I would really have liked to use the Concurrent classes from .NET 4.0, but I’m going to take a stab in the dark here and guess that’s not an option until next year. Which means no SpinLock either. ReaderWriterLockSlim it is!

Refactoring

I will refactor XmlEntityTable to inherit from XmlTable instead of XmlTableBase and reuse what is implemented on XmlTable.

I will try and tidy up the xml storage classes so they can be reused in other projects that need xml database-like storage.

Technorati Tags: ,,,
No comments yet.