September 9, 2015 // By Jeff Ferguson
I maintain an open source project called Gepsio. Gepsio is a processor and validator for eXtensible Business Reporting Language (XBRL) documents. It currently ships as a .NET assembly that developers can use to add XBRL parsing and document validation functionality into their .NET applications. Since XBRL is built on XML, Gepsio leverages the XML services from the .NET Framework and builds its XBRL semantics on top of the XML services.
Over time, Microsoft has released a variety of versions of the .NET Framework, and its XML service strategy has also changed over time. How can the Gepsio code base be structured to offer support for these changes while maintaining the best possible source code design? Visual Studio 2015, with its Shared Projects feature is key. When combined with a code design focused on reusability, Visual Studio 2015’s Shared Projects feature allows Gepsio to support a code base that offers support for a variety of platforms.
We’ll begin by taking a look at some design considerations that favor code reuse. After that, we’ll take a look at how Visual Studio 2015 Shared Projects take advantage of those code reuse design to offer a solution that targets multiple platforms through shared code.
XML Services as a “Data Layer”
Several Platforms, Several XML Service Implementations
Microsoft has, over time, offered various XML service layers in its .NET Framework portfolio. One of them is based on a Document Object Model (DOM) design and the other is a design based on LINQ known as “LINQ-to-XML”. As Microsoft has introduced new hardware and software platforms, various levels of support for its XML services have shipped over time. Additionally, platforms such as Xamarin, which target non-Microsoft platforms such as iOS and Android, offer additional APIs surfaces for XML subsystems. Some developers have even written their own XML parsers and have offered them to the open source community, such as the one at https://github.com/KirillOsenkov/XmlParser.
All of these various API surfaces for XML require some design considerations if Gepsio is to support each of these platforms. XBRL leverages the XML Schema specification for some of its work, for example, but API surfaces such as LINQ-to-XML offer no support for XML schemas. Ideally, Gepsio’s XML service layer must support each of these API surfaces while still providing the XBRL code with the support it needs to get its work done.
XML Services as an Abstraction
We can think of XBRL’s leverage of XML as a design in which XML is a kind of “data layer” and XBRL is a kind of “business layer”. The XBRL “business layer” adds value and semantics on top of the XML “data layer”:
With the understanding that different Microsoft platforms offer potentially different XML service implementations, the Gepsio code base takes a design in which the XML service layer is abstracted away behind a set of interfaces:
With this design in place, Gepsio’s XBRL “business layer” code is neatly abstracted away from the various XML service implementations on the various Microsoft platforms, and Gepsio simply uses the interface implementation available in the shipping assembly. Different interface implementations can be provided for the various implementations:
With a design like this in place, Gepsio can provide interface implementations for additional XML API surfaces without changing the higher-level XBRL business layer:
Using Shared Projects to Build Multiple Platform Implementations
How can this design be used to provide platform-specific implementations of the Gepsio project in an efficient manner? The answer lies in the Shared Projects feature of Visual Studio 2015. Gepsio’s Visual Studio solution layout is as follows:
- the XBRL business logic code and the XML interface definitions are in a single shared project
- the XML interface implementation is in a second shared project
- Gepsio assembly projects are a combination of the XBRL shared project and one of the XML interface implementation shared projects
Take a look at the following Solution Explorer screenshot:
What you see here is two shared projects (ignore the UnitTests shared project for this discussion):
- Xbrl
- SystemXml
The Shared Project named Xbrl contains the XBRL “business layer” code as well as the definitions (but not the implementations) of the XML “data layer” interfaces. The Shared Project named SystemXml contains the implementations of the XML “data layer” interfaces using the classes in the .NET Framework’s System.Xml namespace.
It is the combination of the code in these two Shared Projects that are used to make an assembly. The Gepsio solution also contains a Class Library project, as shown here:
The interesting thing about this Class Library project is that it contains no source code! All of its code comes from the two Shared Projects added to the project as references. The Visual Studio build process pulls the code from the Shared Projects together to run the build for this project, which, in this case, is a .NET 3.5 Class Library assembly. Despite the multiple Shared Projects in use, the build produces a single assembly, making distribution and deployment as simple as can be.
Finding Interface Implementations
The question now becomes: how does Gepsio find the correct implementation for the XML “data layer” interfaces at runtime? The interface definitions and implementations are in separate Shared Projects, so it is impossible to know at compile time about the interface implementation types used in the XML “data layer” Shared Project referenced by the Class Library project. Gepsio uses Reflection at startup to find the classes in the assembly that implement a given interface, and, once the class is found, it is placed in a tiny Inversion-of-Control (IoC) container:
/// <summary> /// A very simple IoC container. /// </summary> internal static class Container { private static Dictionary<Type, Type> registeredTypes; private static Assembly currentAssembly; private static Type[] allTypes; static Container() { registeredTypes = new Dictionary<Type, Type>(); currentAssembly = Assembly.GetExecutingAssembly(); allTypes = currentAssembly.GetTypes(); RegisterAllTypes(); } private static void RegisterAllTypes() { Register<IAttribute>(); Register<IAttributeList>(); Register<IDocument>(); Register<INamespaceManager>(); Register<INode>(); Register<INodeList>(); Register<IQualifiedName>(); Register<ISchema>(); Register<ISchemaElement>(); Register<ISchemaSet>(); Register<ISchemaType>(); } /// <summary> /// Registers an interface with the container. /// </summary> /// <remarks> /// Only the interface need be specified. The methods automatically finds the /// class that implements the interface and associates the class' type in the /// container with the interface. /// </remarks> /// <typeparam name="TInterface"> /// The interface to be registered. /// </typeparam> private static void Register<TInterface>() { var implementationType = FindTypeWithInterfaceImplementation<TInterface>(); registeredTypes.Add(typeof(TInterface), implementationType); } /// <summary> /// Finds the class that implements the given interface and returns its type. /// </summary> /// <typeparam name="TInterface"> /// The interface whose implementation should be found. /// </typeparam> /// <returns> /// The type of class that implements the given interface. /// </returns> private static Type FindTypeWithInterfaceImplementation<TInterface>() { foreach (var currentType in allTypes) { var typeInterfaces = currentType.GetInterfaces(); foreach (var currentInterface in typeInterfaces) { if (currentInterface.Equals(typeof(TInterface)) == true) return currentType; } } throw new TypeLoadException(); } /// Other code removed for brevity, because, /// frankly, this has gone on long enough. :) }
When Gepsio’s XBRL “business layer” needs XML services, it simply goes to the IoC container, as any IoC client would, and says “give me the type that implements the XML interface that is needed at the moment”. It gets a type back, it works with the type through the defined XML “data layer” interface, and goes on about its work.
Scaling the Design to Other Platforms
How does this code design scale to support other compilation platforms? As it turns out, it scales quite well. Suppose, for example, that Gepsio is expanded to support the Universal Windows Platform (UWP). What work would be involved here?
As it turns out, not much. The main work here would be to develop a new XML “data layer” implementation, since not all of the System.Xml classes are available in UWP. If the new XML “data layer” is developed as a separate Shared Project, then it can be worked on independently. This gives us two XML “data layers”: one for .NET and one for UWP.
The XBRL “business layer” does not need to change, however, since the interfaces already in place have abstracted away the XML platform differences from the XBRL code. The Universal Windows assembly project, then, adds two references: the XBRL “business layer” shared project and the UniversalWindowsXML “data layer” shared project.
Viola! We’re now ready to go on a new platform.
See It in Action
The Gepsio code, which includes this design, is available for you to download and take a look at today. You can go to http://gepsio.codeplex.com to see the Gepsio project site out on Codeplex, and navigate to the “SOURCE CODE” tab to grab the latest code. A direct link to the changeset containing all of the source code with this design is available at http://gepsio.codeplex.com/SourceControl/changeset/77191.
If you’d like to contact Magenic, email us or call us at 877-277-1044.