-
Notifications
You must be signed in to change notification settings - Fork 0
Design Philosopy and Architecture
Bridging between a software's internal object model and other data formats (XML, relational DBs, JSON, etc...) is an endeavor riddled with challenges, and very rarely a perfect, uncompromising solution survives the impact with the real world; for this reason MapXML is not meant to handle all kinds of XML structure, and it focuses instead on structures that already closely resemble the CLR object models they need to be mapped to. So either:
- An XML Structure already exists and a CLR object model can be created (or altered) to conform to it,
- or there already is a class structure but there are few constraints on the XML to be produced.
While the library offers a measure of flexibility and it is possible to handle a few corner cases by writing code (see Handler), trying to fit together two radically different structures quickly devolves into a complex, unmaintainable and undebuggable mess, far more complicated than just using a DOM parser.
The most efficient use of the library remains providing annotations through member attributes. Let's look at the minimal example again:
<Movies Owner="Alice" >
<Movie Title="The Fellowship of the Ring" Director="Peter Jackson" ReleaseYear="2001" Genre="Fantasy" />
<Movie Title="The Two Towers" Director="Peter Jackson" ReleaseYear="2002" Genre="Fantasy" Prequel="The Fellowship of the Ring" />
<Movie Title="The Return of the King" Director="Peter Jackson" ReleaseYear="2003" Genre="Fantasy" Prequel="The Two Towers" />
</Movies>
public class MovieCollection
{
[XMLChild("Movie")]
public List<Movie> Movies { get; set; } = new List<Movie>();
[XMLAttribute("Owner")]
public string OwnerName { get; set; }
//Lookup function to help MapXML lookup previously loaded Movies by title
[XMLFunction]
public Movie? GetMovie([XMLParameter("Title")] string t)
{
return Movies.FirstOrDefault(m => m.Title == t);
}
}
public class Movie
{
public string Title { get; set; }
public string Director { get; set; }
public int ReleaseYear { get; set; }
public string Genre { get; set; }
[XMLAttribute("Prequel", DeserializationPolicy.Lookup)]
public Movie Prequel { get; set; }
}
static void Main(string[] args)
{
//Define options
IDeserializationOptions opt =
XMLDeserializer.OptionsBuilder()
.AllowImplicitFields(true) //<-- Allow implicit fields
.Build();
MovieCollection movies = new MovieCollection(); //This will be the owner of the root node
// Create an instance of the XMLDeserializer
XMLDeserializer deserializer = new XMLDeserializer(MovieCollectionSample.XML, Handler: null, RootNodeOwner: movies, Options: opt);
deserializer.Run();
// Inspect the results
Console.WriteLine(movies.OwnerName);
Console.WriteLine(movies.Movies.Count);
}
A few points to notice:
- Not all class members are annotated, only the ones that have a different name than the related XML attributes. MapXML allowes the implicit mapping of all the class members, although this feature is not active by default and should be used with some caution.
- The "ReleaseYear" property of the "Movie" class is integer typed, but no conversion code is specified anywhere; this is because MapXML already handles conversion between String and the standard primitives by default.
- The only bit of mapping code in this example is the XMLFunction that handles the lookup for the "Prequel" attribute. We told the system that the XML attribute is mapped to a "Movie" typed property of the same name, and that the final value should be looked up, but there is no direct mention of the lookup function, and in fact this function is defined in a different class altogether. This is a design choice, because all "Movie" nodes are located within a "MovieCollection" node, the Deserializer should be able to traverse its internal stack and find the lookup function definition without additional boilerplate.
Each step of the Deserialization process requires information to move forward; a few examples:
- Whenever a new node is encountered, the system needs to know whether to create a new object or lookup an existing one, and at the very least its Type
- For each attribute it needs to know whether to convert it to a new Type or leave it a String, and where to store the final value
- Whenever a node is closed the corresponding object instance needs to be stored somewhere
To obtain the most information with the least amount of boilerplate, the system generates a Node Behavior for each new encountered node and keeps it around for as long as that node is still being processed; each nested Node Behavior is pushed in a stack-like structure that lets the system iterate each level until the required information is found.
As a backstop, when it is not possible or practical to define the needed strutures within an enclosing type (frequently to solve corner cases like root node managment) the caller may provide an Handler, which is then asked as a last resort.