Introduction
In the immortal words of Tom Lehrer, "2, 4, 6, 8, time to transsubstantiate!" This article describes my technique for avoiding writing a lot of boiler-plate code to have a derived class implement constructors that simply call the base class' constructors.
Background
If you write a class but don't provide a constructor, the compiler will provide a parameterless constructor for it. If you derive another class from that class and again don't provide a constructor, the compiler will provide a parameterless constructor for it, this constructor will also call the base class' constructor.
If you write a class that has a constructor that takes a parameter, the derived class must also provide a constructor that calls that constructor (the compiler won't add one). In some class hierarchies (as with custom Exceptions and EventArgs), the constructors of the derived classes don't add any functionality, but merely pass the parameters along to the base class constructors.
public class MyBase
{
public MyBase
(
string Name
)
{
...
}
}
public class MyDerived
{
public MyDerived : MyBase
(
string Name
)
: base
(
Name
)
{
}
}
In cases like this, it seems to me that the compiler could add the constructor, but it won't.
Because I have had to write a lot of constructors like this, and recently I was about to write a bunch more, I finally got around to devising some simple code generation to do it.
This technique uses an XML file to give the Transsubstantiate utility instructions on what Assembly to load and which class to query for constructors. Information about the constructors is stored in the XML file and then XSLT can be used to transform the information to code. I include some XSLT that will produce partial classes in C# or Visual Basic; you can modify the XSLT to suit your needs.
For convenience, I also added the ability to copy XML documentation for the class and constructors. This feature is rather rudimentary and I haven't bothered to write the XSLT to do it for Visual Basic.
Using the Code - Part 1
First, write the XML file (e.g. MyDerived.xml):
<Transsubstantiate Assembly="MyLibrary.dll"
XmlDocumentation="MyLibrary.xml" Type="MyLibrary.MyBase">
<Produce Namespace="MyLibrary" ClassName="MyDerived" />
</Transsubstantiate>
Second, execute the utility:
Transsubstantiate MyDerived.xml
This will update the XML file and you can then use XSLT to transform it. To make it easier to use, you can specify an XML stylesheet (XSLT) in the XML file and then Transsubstantiate
will perform the transform:
="text/xsl"="Transsubstantiate.xsl#cs"
<Transsubstantiate Assembly="MyLibrary.dll"
XmlDocumentation="MyLibrary.xml" Type="MyLibrary.MyBase">
<Produce Namespace="MyLibrary" ClassName="MyDerived" /></Transsubstantiate>
In which case, the resultant code will be streamed to the console and you can redirect it to a file:
Transsubstantiate MyDerived.xml > MyDerived.cs
The above XML file specifies the XmlDocumentation
attribute of the Transsubstantiate
element, this attribute is optional. If included, Transsubstantiate
will attempt to copy XML documentation from the specified file, otherwise it won't (XML documentation is not available via Reflection).
Another optional attribute is that the Produce
element may have a BaseClass
attribute. If specified, the provided XSLT will include its contents as the base class of the resultant class. This is generally not needed if you are also writing another part of the partial class that will specify the base class, but may be used if you are not adding any other code to the derived class.
Transsubstantiate
Main
The Main
method of the utility loads the XML document from the specified file, loads the XML Documentation file (if specified), passes them both to the DoTranssubstantiate
method, writes the altered XML document back out to overwrite the original file, and, if the document specifies a stylesheet, attempts to transform the XML document to the console.
[System.STAThreadAttribute()]
public static int
Main
(
string[] args
)
{
int result = 0 ;
try
{
if ( args.Length > 0 )
{
System.IO.FileInfo fi = PIEBALD.Lib.LibFil.GetExpandedFileInfo ( args [ 0 ] ) ;
System.Xml.XmlDocument doc = PIEBALD.Lib.LibXml.LoadXmlDocument ( fi ) ;
System.Xml.XmlDocument xml = null ;
System.Xml.XmlAttribute att ;
if ( ( att = doc.DocumentElement.Attributes [ "XmlDocumentation" ] ) != null )
{
xml = PIEBALD.Lib.LibXml.LoadXmlDocument ( att.Value ) ;
}
result += DoTranssubstantiate ( doc , xml ) ;
PIEBALD.Lib.LibXml.WriteXmlDocument ( doc , fi ) ;
System.Xml.XmlElement xsl = PIEBALD.Lib.LibXsl.GetStylesheet ( doc ) ;
if ( xsl != null )
{
System.Console.WriteLine ( PIEBALD.Lib.LibXsl.Transform ( doc , xsl ) ) ;
}
}
else
{
System.Console.WriteLine ( "Syntax: Transsubstantiate xmlfile" ) ;
}
}
catch ( System.Exception err )
{
System.Console.WriteLine ( err ) ;
}
return ( result ) ;
}
DoTranssubstantiate
This method loads the specified Assembly and gets a reference to the specified Type. It then stores any access modifies (public
, private
, etc.) the Type has to the XML document, calls DrinkTheWine
to copy any XML documentation on the class to the XML document, and calls ChewTheWafer
to access the Type's constructors and write them to the XML document.
private static int
DoTranssubstantiate
(
System.Xml.XmlDocument Document
,
System.Xml.XmlDocument XmlDocument
)
{
int result = 0 ;
System.Xml.XmlElement tran = Document.DocumentElement ;
System.Reflection.Assembly assm = PIEBALD.Lib.LibSys.DynamicLoad
( tran.Attributes [ "Assembly" ].Value ) ;
System.Type type = assm.GetType
( tran.Attributes [ "Type" ].Value ) ;
if ( type == null )
{
throw ( new System.InvalidOperationException
(
"No type named " + tran.Attributes [ "Type" ].Value +
" found in assembly " + tran.Attributes [ "Assembly" ].Value
) ) ;
}
System.Xml.XmlNode ele = tran.SelectSingleNode ( "Attributes" ) ;
if ( ele == null )
{
tran.AppendChild ( ele = Document.CreateElement ( "Attributes" ) ) ;
}
ele.InnerText = PIEBALD.Lib.LibSys.GetPrefices ( type.Attributes ) ;
DrinkTheWine
(
Document
,
XmlDocument
,
tran
,
"/doc/members/member[@name='T:" + tran.Attributes [ "Type" ].Value + "']"
) ;
result += ChewTheWafer
(
Document
,
XmlDocument
,
type
,
tran
) ;
return ( result ) ;
}
ChewTheWafer
Accesses the Type's constructors and writes their relevant details to the XML document. Information about each non-private constructor, its parameters, and any available XML documentation will be written to the XML document.
The code, as it stands now, may not have support for every feature (e.g. optional parameters), but I haven't yet had a need for more than the basics; if you do, you can modify the code and post a message with your improvements.
private static int
ChewTheWafer
(
System.Xml.XmlDocument Document
,
System.Xml.XmlDocument XmlDocument
,
System.Type Source
,
System.Xml.XmlNode Dest
)
{
int result = 0 ;
System.Xml.XmlNode cons = Dest.SelectSingleNode ( "Constructors" ) ;
if ( cons != null )
{
cons.RemoveAll() ;
}
else
{
Dest.AppendChild ( cons = Document.CreateElement ( "Constructors" ) ) ;
}
System.Xml.XmlAttribute att = Document.CreateAttribute ( "Count" ) ;
cons.Attributes.Append ( att ) ;
System.Text.StringBuilder partypes = new System.Text.StringBuilder() ;
foreach
(
System.Reflection.ConstructorInfo ci
in
Source.GetConstructors
(
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance
)
)
{
if ( !ci.IsPrivate )
{
System.Xml.XmlElement con = Document.CreateElement ( "Constructor" ) ;
att = Document.CreateAttribute ( "Attributes" ) ;
att.Value = PIEBALD.Lib.LibSys.GetPrefices ( ci.Attributes ) ;
con.Attributes.Append ( att ) ;
System.Xml.XmlElement pars = Document.CreateElement ( "Parameters" ) ;
att = Document.CreateAttribute ( "Count" ) ;
pars.Attributes.Append ( att ) ;
partypes.Length = 0 ;
foreach
(
System.Reflection.ParameterInfo pi
in
ci.GetParameters()
)
{
System.Xml.XmlElement par = Document.CreateElement ( "Parameter" ) ;
att = Document.CreateAttribute ( "Attributes" ) ;
att.Value = pi.Attributes.ToString() ;
par.Attributes.Append ( att ) ;
att = Document.CreateAttribute ( "IsByRef" ) ;
att.Value = pi.ParameterType.IsByRef.ToString() ;
par.Attributes.Append ( att ) ;
att = Document.CreateAttribute ( "DataType" ) ;
att.Value = pi.ParameterType.FullName.Trim ( '&' ) ;
partypes.AppendFormat ( ",{0}" , att.Value ) ;
par.Attributes.Append ( att ) ;
att = Document.CreateAttribute ( "Name" ) ;
att.Value = pi.Name ;
par.Attributes.Append ( att ) ;
pars.AppendChild ( par ) ;
}
DrinkTheWine
(
Document
,
XmlDocument
,
con
,
"/doc/members/member[@name='M:" + Dest.Attributes
[ "Type" ].Value + ".#ctor(" + partypes.ToString().Trim ( ',' ) + ")']"
) ;
pars.Attributes [ "Count" ].Value = pars.ChildNodes.Count.ToString() ;
con.AppendChild ( pars ) ;
cons.AppendChild ( con ) ;
result++ ;
}
}
cons.Attributes [ "Count" ].Value = cons.ChildNodes.Count.ToString() ;
return ( result ) ;
}
DrinkTheWine
This method copies the specified XML documentation from the source XML documentation document to the Transsubstantiate
XML document.
It is very simplistic, but it does the minimum to contend with some rather irksome XML.
private static void
DrinkTheWine
(
System.Xml.XmlDocument Document
,
System.Xml.XmlDocument XmlDocument
,
System.Xml.XmlNode Parent
,
string Path
)
{
System.Xml.XmlNode xml = Parent.SelectSingleNode ( "XmlDocumentation" ) ;
if ( xml != null )
{
xml.RemoveAll() ;
}
else
{
Parent.AppendChild ( xml = Document.CreateElement ( "XmlDocumentation" ) ) ;
}
if ( XmlDocument != null )
{
int left = Path.IndexOf ( '`' ) ;
if ( left > -1 )
{
left = Path.IndexOf ( '[' , left ) ;
int right = Path.LastIndexOf ( ']' , Path.Length - 2 ) ;
Path = Path.Substring ( 0 , left ) + Path.Substring ( right + 1 ) ;
}
System.Xml.XmlNode ele = XmlDocument.SelectSingleNode ( Path ) ;
if ( ele != null )
{
xml.InnerXml = ele.OuterXml
.Replace ( "=\"E:" , "=\"" )
.Replace ( "=\"F:" , "=\"" )
.Replace ( "=\"M:" , "=\"" )
.Replace ( "=\"N:" , "=\"" )
.Replace ( "=\"P:" , "=\"" )
.Replace ( "=\"T:" , "=\"" ) ;
}
}
return ;
}
Using the Code - Part 2
The ZIP file contains all the code required to build the Transsubstantiate utility. There is also Build.bat which can be used to compile it at the command line (modify the DotNet_3_5 as required to reflect the version of the compiler you are using).
I have also included Transsubstantiate.xsl which contains stylesheets to transform the resultant XML to either C# or VB.NET. The stylesheet for C# will copy the XML documentation to the generated code; the stylesheet for VB.NET will not -- I haven't bothered to try to get it to work.
MyException
I included three XML files that are intended to generate a PIEBALD.Types.MyException
class that derives from System.Exception
. This is a situation where Transsubstantiate
can do everything required to produce the desired class, but the XML file must specify the BaseClass
attribute of the Produce
element for it to work.
- MyException.xml -- does not specify a stylesheet
- MyExceptionCS.xml -- specifies the C# stylesheet
- MyExceptionVB.xml -- specifies the VB.NET stylesheet
History
- 2011-03-29 First submitted