This is post 2 in an ongoing series titled Automatic packages from TFS
- Automatic packages from TFS: #1 - Overall plan
- Automatic packages from TFS: #2 – So how can I write a package file?
- Automatic packages from TFS: #3 – Pipelines and data transformation
- Automatic packages from TFS: #4 – Pipeline component internals
- Automatic packages from TFS: #5 – Wrap up
After last week's initial thoughts I've been beavering away on my prototype, and now have a working version. Which is good, as this series of blog posts wasn't going anywhere useful without that...
So this week I'll address the issue of how you can save XML for a package definition without having any references to Sitecore's DLLs.
If you open the XML for a package definition, you'll find a document a bit like this:
<project> <Metadata> <metadata> <PackageName>Your-Package-Name</PackageName> <Author>Jeremy Davis</Author> <Version>1.0</Version> <Revision/> <License/> <Comment/> <Attributes/> <Readme/> <Publisher/> <PostStep/> <PackageID/> </metadata> </Metadata> <SaveProject>True</SaveProject> <Sources> ... </Sources> <Converter> <TrivialConverter> <Transforms/> </TrivialConverter> </Converter> <Include/> <Exclude/> <Name/> </project>
These elements can be split up into three groups:
<Metadata>
section describes the textual properties of the package<Sources>
section contains a set of either file or item sourcesI'll come back to the first two later on. But first, the base
<project>
and its properties - we need some code to model this structure in a helpful way, and allow automatic creation of the XML from the data the model collects.
Describing the properties themselves is pretty simple. We can create a property for each of the parts of the XML. The complex elements from the XML can be represented by further classes, and the simple elements as basic types.
public class PackageProject { public PackageMetadata Metadata { get; private set; } public bool SaveProject { get; set; } public PackageSources Sources { get; private set; } public PackageConverter Converter { get; private set; } public PackageProject() { Metadata = new PackageMetadata(); SaveProject = true; Sources = new PackageSources(); Converter = new PackageConverter(); } }
We need to be able to turn this object into XML, and then ensure that we can also repeat the same trick on the child objects to get their XML too. And that sounds like a job for an interface:
public interface IToXml { XElement ToXml(); }
Each of the classes we define can implement this. We can add it to the
PackageProject
like so:
public XElement ToXml() { XElement xPackage = new XElement("project"); xPackage.Add(Metadata.Serialise()); xPackage.Add(new XElement("SaveProject", SaveProject)); xPackage.Add(Sources.ToXml()); xPackage.Add(Converter.ToXml()); xPackage.Add(new XElement("Include")); xPackage.Add(new XElement("Exclude")); xPackage.Add(new XElement("Name")); return xPackage; }
It generates a root element, and then adds the correct children. For the
Metadata
,
Sources
and
Converter
the generation of the XML is delegated to the child objects. For
<SaveProject>
the property is written out directly. The
<Include>
,
<Exclude>
and
<Name >
elements are left empty, as I've not found their purpose as yet and they are not populated in the example package definitions I've studied.
This pattern can be repeated for the
Metadata
class:
public class PackageMetadata : IToXml { public string Name { get; set; } public string Author { get; set; } public string Version { get; set; } public string Revision { get; set; } public string License { get; set; } public string Comment { get; set; } public string Attributes { get; set; } public string ReadMe { get; set; } public string Publisher { get; set; } public string PostStep { get; set; } public string PackageID { get; set; } public XElement ToXml() { XElement xInnerMetadata = new XElement("metadata"); XElement xOuterMetadata = new XElement("Metadata", xInnerMetadata); xInnerMetadata.Add(new XElement("PackageName", Name)); xInnerMetadata.Add(new XElement("Author", Author)); xInnerMetadata.Add(new XElement("Version", Version)); xInnerMetadata.Add(new XElement("Revision", Revision)); xInnerMetadata.Add(new XElement("License", License)); xInnerMetadata.Add(new XElement("Comment", Comment)); xInnerMetadata.Add(new XElement("Attributes", Attributes)); xInnerMetadata.Add(new XElement("ReadMe", ReadMe)); xInnerMetadata.Add(new XElement("Publisher", Publisher)); xInnerMetadata.Add(new XElement("PostStep", PostStep)); xInnerMetadata.Add(new XElement("PackageID", PackageID)); return xOuterMetadata; } }
This does basically the same thing to generate the XML contained in the
<metadata>
element. And likewise, the
<Converter>
element can be created with:
public class PackageConverter : IToXml { public XElement ToXml() { return new XElement("Converter", new XElement("TrivialConverter", new XElement("Transforms") ) ); } }
This doesn't need any properties, as I've not found any options that need to be represented.
The last property of the overall package is the
Sources
property. This is a bit different, as it represents a collection. We need to be able to add a number of different source definitions to this. It needs to store the collection and be able to serialise it:
public class PackageSources : IToXml { private List<IPackageSource> _sources = new List<IPackageSource>(); public void AddSource(IPackageSource source) { _sources.Add(source); } public XElement ToXml() { XElement xSources = new XElement("Sources"); foreach (IPackageSource src in _sources) { xSources.Add(src.ToXml()); } return xSources; } }
Since there are two types of source we're interested in modelling here (Static Files and Static Items – I'm not using dynamic sources, though I'm sure those types could be modelled in a similar way if you wanted) we need a base type for our representations of these. The
IPackageSource
interface describes the two things we need to keep track of - their names, and a mechanism for adding new items to them:
public interface IPackageSource : IToXml { string Name { get; set; } void Add(string entry); }
It also inherits from our serialisation interface, since as per the pattern above these package sources need to be able to represent themselves as XML.
Both concrete types for sources are similar (and to be honest I'm looking at these and thinking that there's a refactoring job for me to do here) except that they need to generate slightly different XML. The Sitecore Item source looks like:
public class PackageSourceItems : IPackageSource { private List<string> _entries = new List<string>(); public string Name { get; set; } public PackageItemToEntryConverter Converter { get; private set; } public PackageSourceItems() { Converter = new PackageItemToEntryConverter(); } public void Add(string entry) { _entries.Add(entry); } public XElement ToXml() { XElement xItems = new XElement("xitems"); XElement xEntries = new XElement("Entries"); foreach (string entry in _entries) { xEntries.Add(new XElement("x-item", entry)); } xItems.Add(xEntries); xItems.Add(new XElement("SkipVersions", false)); xItems.Add(Converter.ToXml()); xItems.Add(new XElement("Include")); xItems.Add(new XElement("Exclude")); xItems.Add(new XElement("Name", Name)); return xItems; } }
and to generate the right XML, the converter type looks like:
public class PackageItemToEntryConverter : IToXml { public XElement ToXml() { return new XElement("Converter", new XElement("ItemToEntryConverter", new XElement("Options", new XElement("BehaviourOptions", new XElement("ItemMode", "Undefined"), new XElement("ItemMergeMode", "Undefined") ) ) ) ); } }
Similarly the disk files source looks like:
public class PackageSourceFiles : IPackageSource { private List<string> _entries = new List<string>(); public string Name { get; set; } public PackageFileToEntryConverter Converter { get; private set; } public PackageSourceFiles() { Converter = new PackageFileToEntryConverter(); } public void Add(string entry) { _entries.Add(entry); } public XElement ToXml() { XElement xFiles = new XElement("xfiles"); XElement xEntries = new XElement("Entries"); foreach (string entry in _entries) { xEntries.Add(new XElement("x-item", entry)); } xFiles.Add(xEntries); xFiles.Add(Converter.ToXml()); xFiles.Add(new XElement("Include")); xFiles.Add(new XElement("Exclude")); xFiles.Add(new XElement("Name", Name)); return xFiles; } }
and its converter looks like:
public class PackageFileToEntryConverter : IToXml { public XElement ToXml() { return new XElement("Converter", new XElement("FileToEntryConverter", new XElement("Root", "/"), new XElement("Transforms") )); } }
With all that code slotted together, we can write some code that tries to use these assorted types to generate a package definition:
var prj = new PackageProject(); prj.Metadata.Name = "TestPackage"; prj.Metadata.Version = "1.0"; var files = new PackageSourceFiles(); files.Name = "Files to deploy"; files.Add("/somefolder/file1.css"); files.Add("/bin/thebinary.dll"); prj.Sources.AddSource(files); var items = new PackageSourceItems(); items.Name = "Items to deploy"; items.Add("/sitecore/content/something/{DD5E504F-5FF9-477F-A2FB-B3905B76368C}/invariant/0"); prj.Sources.AddSource(items); var xml = prj.ToXml();
And the XML generated comes out as:
<project> <Metadata> <metadata> <PackageName>TestPackage</PackageName> <Author /> <Version>1.0</Version> <Revision /> <License /> <Comment /> <Attributes /> <ReadMe /> <Publisher /> <PostStep /> <PackageID /> </metadata> </Metadata> <SaveProject>true</SaveProject> <Sources> <xfiles> <Entries> <x-item>/somefolder/file1.css</x-item> <x-item>/bin/thebinary.dll</x-item> </Entries> <Converter> <FileToEntryConverter> <Root>/</Root> <Transforms /> </FileToEntryConverter> </Converter> <Include /> <Exclude /> <Name>Files to deploy</Name> </xfiles> <xitems> <Entries> <x-item>/sitecore/content/something/{DD5E504F-5FF9-477F-A2FB-B3905B76368C}/invariant/0</x-item> </Entries> <SkipVersions>false</SkipVersions> <Converter> <ItemToEntryConverter> <Options> <BehaviourOptions> <ItemMode>Undefined</ItemMode> <ItemMergeMode>Undefined</ItemMergeMode> </BehaviourOptions> </Options> </ItemToEntryConverter> </Converter> <Include /> <Exclude /> <Name>Items to deploy</Name> </xitems> </Sources> <Converter> <TrivialConverter> <Transforms /> </TrivialConverter> </Converter> <Include /> <Exclude /> <Name /> </project>
Which (other than the fact that those are not real files or items) is a valid package definition.
Now it's worth emphasising that I'm taking deliberate short-cuts here. I only need this code to be able to produce a simple package containing a set of static files and items. But the tests I've conducted so far suggest that it meets that objective.
So next week we can move on to thinking about the pipeline and some components for it which can transform the TFS data into valid data for our our package...
↑ Back to top