Monday, 6 June 2011

Introducing the (re-vamped) Maestro API: Part 2

For part 2 of this series I am going to talk about the overall design of the Maestro API.

Having used FDO for quite some time now, a lot of its API design has influenced my design of the Maestro API. Those who have used FDO may notice many parallels in the design when working with the Maestro API.

The first similarity is the heavy use of interfaces. FDO models its connections and commands via interfaces. In the Maestro API, we model the following in the same manner using interfaces:
  • MapGuide Server connections
  • MapGuide Service APIs
  • MapGuide Resource classes and message types
By having these types as interfaces, it allows the implementation of these interfaces to be totally different, and I will show later on in this post where having this quality is extremely beneficial.

The MapGuide Server connection is represented by the IServerConnection interface. This is the main point of entry in the Maestro API.

This contains the ResourceService and FeatureService interfaces to provide resource manipulation and feature querying capabilities. Additional MapGuide services are available via GetService method and you can interrogate the Capabilities property to determine what you can and can't do with this particular connection.

Now the reason this is an interface is because in the Maestro API, there are currently 2 implementations of this connection:
  • One that communicates with the mapagent via http. (Maestro.Http)
  • One that wraps the official MapGuide API. (Maestro.LocalNative)
Because this is an interface, the client application can use the same code regardless of implementation. As there is an implementation that wraps the official MapGuide API, you can even use MapGuide Maestro without even needing a web tier because MapGuide Maestro works against the interfaces defined in the Maestro API.

Now given that IServerConnection is an interface, how does one go about creating instances of it? Enter the ConnectionProviderRegistry

Simply call CreateConnection passing in the desired provider and the parameters needed to initialize it. The ConnectionProviderRegistry reads from a ConnectionProviders.xml file which contains all the registered connection providers. Each connection provider is an implementation of the IServerConnection interface.

So having acquired a connection, we can now interact with the services provided by MapGuide. Once again, these services are modeled as interfaces

Each service is defined in a corresponding interface. For reference, they are:
  • IFeatureService
  • IResourceService
  • IDrawingService
  • IMappingService (also contains rendering service APIs)
  • IFusionService
  • ITileService
  • ISiteService
Each service interface contains methods which correspond to what you see in the mapagent test page. These services are obtained via the GetService method on IServerConnection. For IResourceService and IFeatureService, these already exist as properties so you can access them directly. Once again because these are interfaces, the implementations can be wildly different:
  • For the http connection provider, these interfaces wrap http requests to the mapagent and automatically processes the XML responses from them.
  • For the local connection provider, these interfaces wrap the offical MapGuide service classes (eg. The implementation of IFeatureService wraps the methods of the MgFeatureService class, and does automatic conversion of MgByteReader values)
Finally, there is MapGuide's resource classes and message types. The MapGuide Server returns many different types of XML data. But with the Maestro API, instead of working with raw XML content and having to write a lot of boilerplate code, you get to work with strongly typed classes and interfaces, with XML serialization automatically handled for you

Now there's a lot of XML Schemas for all the various forms of XML data that MapGuide returns. Actually building the classes that represent this data by hand would've taken an eternity, so I've taken a shortcut for this particular case and used the xsd2code code generator to generate all the required classes from their XML Schemas. I used xsd2code for generating the classes instead of using the default xsd.exe tool for reasons I have already explained. The set of generated classes produced by xsd2code a much cleaner (relative to xsd.exe anyway) and gave me a very useful foundation to work with.

In the case of resource classes, using the generated classes as-is was still not good enough. Additional tweaking of this generated code was required. The reason is that each particular version of a resource was its own separate class when generated. Each particular version of a resource in its generated class form had nothing in common. Once again, interfaces come to save the day.

Now you may notice that there is only one interface for Layer Definitions, but there's been 3 additional revisions to date in the Layer Definition XML schema (ref 1, 2, 3, 4 and 5). So how were 4 separeately generated classes able to be reconciled into a single interface?

One of the fortunate things thus far, that Autodesk did when they introduced a new version of a given resource was that the schema revisions have been incremental and additive. Nothing is actually removed or altered in every schema revision thus far.

Because of this, the design of these interfaces is such that they represent the 1.0.0 version of the respective resource type each interface is modeling. Elements introduced in newer versions of the schema are exposed through a new version of the interface that extends the original.

So with this design, we have a consistent baseline interface for all resource types and we can interrogate the resource version to determine if we can cast these interfaces (or its child properties, which are also interfaces) to the newer versions to tap into the extra functionality exposed in these newer interfaces.

As is the case with the Web Layout in the above diagram. IWebLayout2 extends IWebLayout and maps to the 1.1.0 version of the Web Layout schema that introduced a new "ping server" property. However both can be accessed via the IWebLayout interface. We can check if the resource version is 1.1.0 to determine if it can be cast into the IWebLayout2 interface.

It is through this design that the Resource Editors in MapGuide Maestro are able to edit most, if not all known versions of a given resource type, and have the necessary intelligence to enable/disable certain UI elements for elements not supported by the edited resource version. This design allows Maestro to keep up to date as newer resource types or schema versions are made available, providing that these schema revisions stay true to their additive form.

This incremental quality of the XML schema revisions is also reflected in the generated code. Thus with the magic of partial classes and careful use of the C# pre-processor, getting different versioned classes to implement the same baseline interface was actually an easy task. Then it was a case of extracting the newer properties and methods introduced in newer schema versions into their own separate interface that extends the original.

The end result, is a consistent object-oriented API that is (mostly :) ) free of boilerplate code that you would normally write to deal with raw XML content, MgByteReaders and HTTP requests/responses. Any developer who has worked with the official MapGuide API would appreciate

So that's the high-level overview of the design of the Maestro API. Stay tuned for part 3, where I actually show some concrete examples of how to use this library.

No comments: