Overview

    An XObject is a simple, type safe wrapper around any Xml. It provides simpler and more elegant code for accessing a known XDoc schema, and lends itself to easier refactoring. To create an XObject, simply create an interface implementing IXObject that describes the data model you wish to represent. For example, given this pseudo Xml Schema:

    <user id="...">
        <name>...</name>
        <dob>...</dob>
    </user>

    A simple data model might look like this:

    public interface IUser : IXObject {
        int id { get; set; }
        string Name { get; set; }
    
        [XObjectPath("dob")]  
        DateTime DateOfBirth { get; set; }
    }

    This interface can now be used to create, modify and parse the appropriate Xml

    // create a new instance without an existing document
    IUser user = XObjectBuilder.New<IUser>();
    user.id = 1;
    user.Name = "Bob";
    user.DateOfBirth = DateTime.Parse("10/10/74");
    
    // create an instance from a document
    XDoc doc = user.AsDocument;
    IUser user1 = XObjectBuilder.FromXDoc<IUser>(doc);
    Console.WriteLine("Name: {0}", user1.Name);

    Creating and using XObjects

    The XObjectBuilder

    The XObjectBuilder static class provides methods for creating instances from IXObject interfaces.

    Create an instance from an existing document

    You can either create the instance from an Xml string:

    T xObject = XObjectBuilder.FromXDoc<T>(XDoc doc);

    or from an existing XDoc instance:

    T xObject = XObjectBuilder.FromXml<T>(string xml);

    Create an instance without a source document

    T xObject = XObjectBuilder.New<T>();

    Since this creates a new document, the root node will be inferred from the interface name. In order to override this behavior the XObjectRoot attribute can be used like this:

    [XObjectRoot("user")]
    public interface IUser : IXObject {
      ...
    }

    Defining XPath's by Naming Convention

    By default XObjectBuilder will infer an xpath based on simple naming conventions. The rules used for xpath inference are:

    • The Accessor name will be lower-cased (Foo becomes foo)
    • CamelHumps insert hyphens in the lower-cased name (i.e. FooBar becomes foo-bar)
    • Underscores are replaced with / for Xpath (i.e. Foo_Bar becomes foo/bar)
      • Leading and trailing underscores are ignored
    • A lower case accessor is treated as an attribute (i.e. foo becomes @foo)
      • the only lower case considered as attribute is the very last segment of the name

    Of course these conventions could be combined so that an accessor named Foo_barBaz becomes foo/@bar-baz.

    <user id="...">
        <name>...</name>
    </user>

    Can be represented by the model

    public interface IUser : IXObject {
        int id { get; }
        string Name { get; }
    }

    Basic Attribute markup

    If the naming conventions are not sufficient for XPath inference, or the different naming conventions are desired, the XObjectPath attribute can be used on an accessors to define an exact xpath.

    [XObjectPath("name/first)]
    public string FirstName { get; set; }

    Built-in Accessor Types

    Simple types

    int, uint, string, double, float, decimal, byte, byte[], long

    Complex types

    XDoc, IXObject

    Collections of built-in types T

    Arrays, IEnumerable<T>, IList<T>

    XObject's as Accessor Types

    In order to represent deeper hierarchies of Xml, an IXObject Accessor can be another IXObject, allowing for the creation of arbitrarily complex object graphs.

    For example

    <user>
        <name>...</name>
        <email>...</email>
        <home-address>
            <street>...</street>
            <city>...</city>
        </home-address>
        <work-address>
            <street>...</street>
            <city>...</city>
        </work-address>
    </user>

    could be consumed and represented by the following two interfaces

     

    public interface IUser : IXObject {
        public string Name { get; set; }
        public string Email { get; set; }
        public IAddress HomeAddress { get; set; }
        public IAddress WorkAddress { get; set; }
    }
    
    public interface IAddress : IXObject {
        public string Street;
        public string City;
    }

    XObject Behavior Caveats

    There are same odd behaviors to keep in mind with the default implementation of IXObject (default, because the entire IXObject system may have different implementation provided if used through an IoC, as described below). These behaviors are artifacts of the underlying data-model being a live XDoc and modifications to this document are done in a lazy fashion.

    Underlying XDoc is only re-written when access to the XDoc is invoked

    In the interest of efficiency IXObject does not clone the source XDoc, but assumes that it's the authoritative owner. If an existing XDoc is used to create an IXObject, it's possible to hold on to the original XDoc and continue to modify it, but this will produce unexpected results. XObject uses auto-initialized backing fields to store values set on the instance and the underlying XDoc is only modified is the AsDocument or this[xpath] accessors are invoked.

    Holding on to references of Accesor values is not recommended

    The behavior is not deterministic. The value may change as other accesses occur, and conversely changes to the references may or may not affect the XObject, since whether it's a lazy reference or a new instance of the value cannot be determined.

    XDoc Accessor types may have their root rewritten

    An accessor of type XDoc assumes that the doc's root is named the same as the last element part of the Accessor's XPath. Generally, using XDoc, root names are ignored anyhow, so this is not much of an issue. However, the above caveat of lazy writing applies here as well, i.e. if the Accessor is set, that Accessor will continue to reference the set XDoc (and it's incorrect root) until the XDoc is rewritten.

    Advanced XObject Extensibility

    The IXObject interface

    The IXObject interface is the base for all XObjects, providing basic access to the underlying XDoc.

    public interface IXObject {
        XDoc this[string path] { get; }
        XDoc AsDocument { get; }
    }

    XObjects neither loose any of the original data, even if not exposed, nor do they fail if the XPath of an Accessor defined on the interface does not exist in the XDoc. XObject is not meant to validate or enforce schema, simply to provide an interface to the data model that is easier to read, type safe and more friendly to C# programming.

    XObject Attributes

    XObjectRoot

    [XObjectRoot(string rootElement)]

    XObjectRoot can only specified on the interface level itself and defines the XDoc root element name for XObjectBuilder.New to use. If the attribute is omitted the same inference convention is used as for inferring xpath. Since this is a single element, an interface relying on convention cannot have a name containing an underscore. In addition, any leading I (the capital letter "i") is removed prior to inference.

    Whether inferred by convention or declared by XObjectRoot, the root element value is only used for creating a new XDoc and is not a constraint for any document passed into a builder. XObjectBuilder does not do any validation of a passed XDoc nor does it rewrite the root of a wrapped XDoc when conve back into an XDoc.

    XObjectPath

    [XObjectPath(string xpath)]

    XObjectPath provides the XPath at which the value of the XPath should be found. It is not limited to attribute or child elements of the root element, but can fetch any node accessible by a valid XPath expression. For example, given

    <html>
        <head>
            <title>Foo</title>
        </head>
        ...
    </html>

    The title could be exposed via an Accessor like this:

    [XObjectPath("head/title")
    string Title { get; }

    XObjectTypeConverter

    XObject has a number of automatic converters built in:

    • Any value that has an AsType accessor on XDoc (except InnerText and XmlNode). These types are supported both in nullable and value type form (with automatic default)
    • Enums and their nullable counterpart (defaults to first enum value if node is empty, throws ArgumentException on bad value)
    • XDoc

    • Any IXObject

    However should more conversion to another datatype be needed or the default conversion mechanism needs to be altered, then

    XObjectTypeConverter

    attribute can be used to provide a custom type converter.

    [XObjectTypeConverter(Type converterType)]

    converterType must implement IXObjectTypeConverter<T>.

    In addition XObjectTypeConverter markup on a XObject supported collection accessor is used to convert the collection's type not the collection itself.

    Converter Lifestyle

    Converters are considered to be singleton, i.e. they should never try to maintain state for a value of an accessor. Once a converter is built it will be re-used for other accessors requiring the same type. There are two levels to this, however, converters registered with the repository (an optional capability discussed below) are shared among all XObjectBuilders in that repository, while converters not found in the repository are only shared in that particular builder (although that means, still shared for all instances created by the builder).

    If an accessor has converter markup, it simply tells the builder to either find an existing instance of that converter or create one and add it to the converter cache. I.e. two accessors referencing the same converter type will use the same converter instance.

    IXObjectTypeConverter

    This attribute can be used to decorate the accessor directly. Alternatively, the attribute can decorate the IXObject in which case the first converter that returns true for CanConcert on the accessors type returns true.

    Converters must contain a no-argument constructor in order to be auto-generated, unless an instance to the converter was provided to the XObjectBuilder<T> by its repository.

    public interface IXObjectTypeConverter {
        bool CanConvert(Type t);
        object Convert(XDoc doc);
        bool CanConvertBack { get; }
        void ConvertBack(XDoc doc, object value);
    }
    CanConvert

    CanConvert is the first check of fitness of a converter for an accessor type and must return true even it the converter was attached to the accessor by direct markup.

    Convert

    Convert must always be implemented as it provides the type conversion for the Accessor getter.

    ConvertBack and CanConvertBack

    ConvertBack implements the Accessor setter. The XDoc provided is just the node addressed by the Accessor's XPath. ConvertBack may be left unimplemented as long as CanConvertBack returns false

    Built-in Type Converters
    XObjectNotEmptyConverter

    This converter simply checks whether the XDoc returned for the Accessor is empty and returns a boolean. The facilitates the behavior where existence of a certain value is the significant attribute, such as a list of capabilities that should be turned into a set of true/false flags

    XObjectEnumConverter

    Most of the time this converter is automatically used for Accessors with enum or nullable enum types. However, if a custom default value is required, XObjectEnumConverter can be constructed manually and provided a default. In this case it must be provided to a custom XObjectBuilder<T> (see below).

    XObjectXPathDocBuilder

    This attribute allows the specification of altnerate document builders for XPath expressions that the built in builder cannot handle (which is anything beyond basic element/attribute paths to single nodes). The attribute specifies a class implementing the below interface:

    public interface IXPathDocBuilder {
        XDoc BuildXDoc(XDoc rootDoc, string xpath, int nodeCount);
        bool CanHandle(string xpath, bool isNodeCollection);
    }

    Like XObjectPath, this attribute can decorate either an accessor directly or be used on the IXObject level. At the accessor level it dictates that the specified builder is used on set operations that cannot locate an existing node at the XPath. When specified on the interface level, it simply enumerates builders that are iterated through at construction time until the first one satisfying CanHandle is found. The default builder is always first and the rest are processed in the order of the attributes on the interface.

    Collection Accessors

    XObjectBuilder supports four types of collections natively:

    • Array
    • IEnumerable<T>
    • ICollection<T>
    • IList<T>

    All collections except IEnumerable<T> require a type that has native support for a setter or have an XObjectTypeConverter<T> that provides a ConvertBack delegate. While Array may seem like a read-only collection, it still allows the setting of items in the array.

    Advanced Object Graphs

    Any IXObject can be a return type of an IXObject Accessor and it will automatically be built from the original XDoc.

    <user>
        <name>...</name>
        <groups>
            <group id="...">
                <name>...</name>
                <permissions>
                    <permission>read</permission>
                    <permission>write</permission>
                </permissions>
            </group>
        <groups>
    </user>

    The above document could be wrapped by a contract like this:

    public interface IUser : IXObject {
        
        string Name { get; }
        IList<IGroup> Groups { get; }
    }
    
    public interface IGroup : IXObject {
        
        string Name { get; }
        
        [XObjectPath("permissions[permission='read']")]
        [XObjectTypeConverter(typeof(XObjectNotEmptyConverter))]
        bool CanRead { get; }
    
        [XObjectPath("permissions[permission='write']")]
        [XObjectTypeConverter(typeof(XObjectNotEmptyConverter))]
        bool CanWrite { get; }
    }

    Notice that IGroup has no coupling to IUser, but simply assumes that it will be provided an XDoc that is rooted such that i can find the name and permissions elements.

    The XObjectBuilder<T> and XObjectBuilderRepository

    By default the builder system for IXObject is hidden behind static accessors on XObjectBuilder. However, behind the scenes, a repository of builders is cached, both for efficiency and because the nature of IXObject graphs requires that the same builders are accessible for construction of subsequent objects. The repository system can be manually accessed if more control over the XPathDocBuilders and Converters is needed, or if XObjectBuilder is to be used in an Inversion of Control Container.

    Static Utility Members

    The XObjectBuilder class exists a utility shortcut for default builders. It contains static accessors for common operations as well as a registry that caches builders for existing types.

    New<T>
    T xObject = XObjectBuilder.New<T>();
    FromXDoc<T>
    T xObject = XObjectBuilder.FromXDoc<T>(XDoc doc);
    FromXml<T>
    T xObject = XObjectBuilder.FromXml<T>(string xml);
    SetRepository
    // assuming that the repository is pulled from an IoC container
    IXObjectBuilderRespository repository = container.Resolve<IXObjectBuilderRespository>();
    XObjectBuilder.SetRepository(repository);

    XObjectBuilder creates an instance of the default repository XObjectBuilderRepository at creation. SetRepository is only required if the default repository needs to be replaced with a custom repository and the static accessors are still used.

    Custom IXObjectBuilderRepositories

    The IXObjectBuilderRepository is both a container and a factory for XObjecBuilders.

    public interface IXObjectBuilderRepository {
        IEnumerable<IXPathDocBuilder> DocBuilders { get;}
        IEnumerable<IXObjectTypeConverter> TypeConverters { get; }
        XObjectBuilder<T> GetBuilder<T>() where T : IXObject;
        object GetBuilder(Type t);
    }
    Using a manually constructed Default

    The default implementation, XObjectBuilderRepository, also allows the registration of XPathDocBuilders and XObjectTypeConverters to be used by all XObjectBuilders.

    void RegisterDocBuilder(IXPathDocBuilder docBuilder);
    void RegisterConverter<T>(IXObjectTypeConverter<T> typeConverter);

    By creating an instance manually and setting it on XObjectBuilder, the helpers can be set on the repository to be used by the static methods.

    Using the Repository with an Inversion of Control Container

    In the context of an IoC, a simple implementation of IXObjectBuilderRepository would allow the repository to be used as a factory provider for XObjectBuilder<T>. In this manner, container controlled code would no longer rely on the static XObjectBuilder accessors, but instead receive the desired XObjectBuilder<T> through constructor injection.

    There exists no general implementation of this version of the repository, since it generally would reach into the parent container that ties it to that particular container and needs to be implemented accordingly.

    Tag page
    Viewing 5 of 5 comments: view all
    This sounds very interesting!, but I can't found the code in github/dream.
    Can you share the code of this?
    Posted 09:40, 11 Oct 2011
    I think it got lost on some old SVN branch. Let me see if i can dig it up and put it on github.
    Posted 10:01, 11 Oct 2011
    Thanks!
    Posted 02:58, 12 Oct 2011
    XObject has landed at github: https://github.com/MindTouch/XObject
    Posted 21:15, 16 Oct 2011
    Great! i will prove it!. I have seen a lot of projects that allow to access xml documents using dynamic types, but i think this is much better.
    Posted 01:28, 17 Oct 2011
    Viewing 5 of 5 comments: view all
    You must login to post a comment.

    Copyright © 2011 MindTouch, Inc. Powered by