Want to show your appreciation? Please to my charity.

Monday, October 26, 2009

Generating Interface, Typical Implementation and Decorator of Value Objects

Things To Be Generated

In one of design session, we all felt being repeatedly writing interface, implementation and decorator class for simple value objects that we use. Let’s say way have interfaces below:

    public interface IBase
    {
        IPartOne PartOne { get; set; }
        int BaseInt { get; set; }
        string BaseString { get; set; }
    }
 
    public interface IPartOne
    {
        int PartOneInt { get; set; }
        string PartOneString { get; set; }
    }
 
    public interface ISub : IBase
    {
        int SubInt { get; set; }
        string SubString { get; set; }
 
        IPartTwo PartTwo { get; set; }
    }
 
    public interface IPartTwo
    {
        int PartTwoInt { get; set; }
        string PartTwoString { get; set; }
    }

And then we write implementations

    public class Base : IBase
    {
        public IPartOne PartOne { get; set; }
        public int BaseInt { get; set; }
        public string BaseString { get; set; }
    }
 
    public class PartOne : IPartOne
    {
        public int PartOneInt { get; set; }
        public string PartOneString { get; set; }
    }
 
    public class PartTwo : IPartTwo
    {
        public int PartTwoInt { get; set; }
        public string PartTwoString { get; set; }
    }
 
    public class Sub : Base, ISub
    {
        public int SubInt { get; set; }
        public string SubString { get; set; }
        public IPartTwo PartTwo { get; set; }
    }

And then we wrote decorators so that it can be XML serialized:

    public class BaseDeco : IBase
    {
        private readonly IBase _base;
 
        public IBase Base
        {
            get { return _base;}
        }
 
        public BaseDeco() : this(new Base()) 
        {
        }
 
        public BaseDeco(IBase @base)
        {
            _base = @base;
        }
 
        IPartOne IBase.PartOne
        {
            get { return _base.PartOne; }
            set { _base.PartOne = value; }
        }
 
        public PartOneDeco PartOne
        {
            get { return new PartOneDeco(_base.PartOne); }
            set { _base.PartOne = value.One; }
        }
 
        public int BaseInt
        {
            get { return _base.BaseInt; }
            set { _base.BaseInt = value; }
        }
 
        public string BaseString
        {
            get { return _base.BaseString; }
            set { _base.BaseString = value; }
        }
    }
 
    public class PartOneDeco : IPartOne
    {
        private readonly IPartOne _one;
 
        public PartOneDeco() : this(new PartOne())
        {
        }
 
        public PartOneDeco(IPartOne one)
        {
            _one = one;
        }
 
        public int PartOneInt
        {
            get { return _one.PartOneInt; }
            set { _one.PartOneInt = value; }
        }
 
        public string PartOneString
        {
            get { return _one.PartOneString; }
            set { _one.PartOneString = value; }
        }
 
        public IPartOne One
        {
            get { return _one; }
        }
    }
 
    public class SubDeco : BaseDeco, ISub
    {
        private readonly ISub _sub;
 
        public SubDeco() : this(new Sub())
        {
        }
 
        public SubDeco(ISub sub) : base(sub)
        {
            _sub = sub;
        }
 
        public int SubInt
        {
            get { return _sub.SubInt; }
            set { _sub.SubInt = value; }
        }
 
        public string SubString
        {
            get { return _sub.SubString; }
            set { _sub.SubString = value; }
        }
 
        IPartTwo ISub.PartTwo
        {
            get { return _sub.PartTwo; }
            set { _sub.PartTwo = value; }
        }
 
        public PartTwoDeco PartTwo
        {
            get { return new PartTwoDeco(_sub.PartTwo); }
            set { _sub.PartTwo = value.Two; }
        }
    }
 
    public class PartTwoDeco : IPartTwo
    {
        private readonly IPartTwo _two;
 
        public IPartTwo Two
        {
            get { return _two; }
        }
 
        public PartTwoDeco(IPartTwo two)
        {
            _two = two;
        }
 
        public int PartTwoInt
        {
            get { return _two.PartTwoInt; }
            set { _two.PartTwoInt = value; }
        }
 
        public string PartTwoString
        {
            get { return _two.PartTwoString; }
            set { _two.PartTwoString = value; }
        }
    }
I guess you are probably bored reading them. But there is more, we also have another decorator class for track the changes and raise property change events.

Using T4 to Generate Code

Naturally, code generation, be it compile time or runtime, is what we need. I search around to find a few options:

  1. Commercial product like CodeSmith - Haven’t look any yet.
  2. CodeDom - I used CodeDom in my Web Service Code Generator Project. It is powerful but quite complex to use.
  3. T4 (Text Template Transformation Toolkit – This is the one we are going to give it a try today.
  4. Dynamic proxy using Spring.Net or Castle DP - We have already using Spring.Net dynamic proxy to generate the decorators for property change events and change tracking. With some limitations.
  5. Write our own dynamic proxy using Reflection.Emit API. I used that before, it is also complex but maybe an alternative to option 4 to overcome the limitations. I’ll save this for another post.

After spending a few hours on Oleg Sych’s blog, I started to write a template for produce the code I shown above. The core piece is BvoGeneratorTemplate.tt, which is listed in the end of this post. Let’s take a look at what it can do.

To generate IPartOne and its implementations, all I need to is to add a T4 script file PartOne.tt with below content:

<#@ import namespace="System.Collections.Generic" #>
<#@ template language="C#v3.5" hostspecific="True" debug="True" #>
<#@ output extension="generated.cs" #>
<#@ include file="T4Toolbox.tt" #>
<#@ include file="BvoGeneratorTemplate.tt" #>
 
<#
    var template = new BvoGeneratorTemplate{
     Name = "PartTwo",
     Properties = new List<Property>
        {
            new Property{Type = "int", Name = "PartTwoInt"},
            new Property{Type = "string", Name = "PartTwoString"},
        },
    };
    template.Render();
#>

That automatically generates the code below includes the interface, basic implementation and the decorator class for XML serialization.

// <autogenerated>
//   This file was generated from T4 template BvoGeneratorTemplate.tt.
// </autogenerated>
 
 
#pragma warning disable 1591
namespace ClassLibrary1
{
    public interface IPartTwo 
    {
        int PartTwoInt { get; set;  }
        string PartTwoString { get; set;  }
    }
 
    public partial class PartTwo : IPartTwo
    {
        [CoverageExclude]
        public virtual int PartTwoInt { get; set;  }
        [CoverageExclude]
        public virtual string PartTwoString { get; set;  }
    }
}
namespace ClassLibrary1.Xml
{
    [CoverageExclude]
    public class PartTwoDeco : IPartTwo
    {
        private readonly IPartTwo _partTwo;
        
        public PartTwoDeco(IPartTwo @partTwo)
        {
            _partTwo = @partTwo;
        }
        
        public PartTwoDeco() : this(new PartTwo())
        {
        }
        
        public IPartTwo PartTwo
        {
            get { return _partTwo; }
        }
        public int PartTwoInt 
        {
            get { return _partTwo.PartTwoInt; }
            set { _partTwo.PartTwoInt = value; }
        }
        public string PartTwoString 
        {
            get { return _partTwo.PartTwoString; }
            set { _partTwo.PartTwoString = value; }
        }
    }
}
 
#pragma warning restore 1591

Next let’s take a look at a most complex example Sub.tt:

  1. Sub inherits from Base
  2. Has documentations
  3. A read only computed property
  4. Has a property with a type also generated  by us
<#@ import namespace="System.Collections.Generic" #>
<#@ template language="C#v3.5" hostspecific="True" debug="True" #>
<#@ output extension="generated.cs" #>
<#@ include file="T4Toolbox.tt" #>
<#@ include file="BvoGeneratorTemplate.tt" #>
 
<#
    var template = new BvoGeneratorTemplate {
        Name = "Sub",
        Base = "Base",
        Document = @"
<Summary>
Documentation for ISub
</Summary>
",
         Properties = new List<Property>
        {
            new Property{Type = "int", Name = "SubInt", Document = @"
<Summary>
String property in Sub
</Summary>
"},
            new Property{Type = "string", Name = "SubString"},
            new Property{Type = "IPartTwo", Name = "PartTwo", IsBvo = true},
            new Property{Type = "bool", Name = "IsLarge", IsComputed = true, HasSetter = false},
        },
    };
    template.Render();
#>

Below is the generated code and there are a few things worth mentioning.

  1. There is no implementation for property IsLarge in the Sub class. Developer need to add a regular Sub.cs file to complete the partial class.
  2. In the SubDeco, the IsLarge property is marked as XmlIgnore because a computed property shouldn’t be serialized.
  3. Explicit interface implementation is used for property PartTwo with return type of IPartTwo. And a real PartTwo property is added with return type of PartTowDeco. Because XMLSerializer  doesn’t serialize property of interface type.
// <autogenerated>
//   This file was generated from T4 template BvoGeneratorTemplate.tt.
// </autogenerated>
 
 
#pragma warning disable 1591
namespace ClassLibrary1
{
    /// <Summary>
    /// Documentation for ISub
    /// </Summary>
    public interface ISub : IBase 
    {
        /// <Summary>
        /// String property in Sub
        /// </Summary>
        int SubInt { get; set;  }
        string SubString { get; set;  }
        IPartTwo PartTwo { get; set;  }
        bool IsLarge { get;  }
    }
 
    public partial class Sub : Base, ISub
    {
        [CoverageExclude]
        public virtual int SubInt { get; set;  }
        [CoverageExclude]
        public virtual string SubString { get; set;  }
        [CoverageExclude]
        public virtual IPartTwo PartTwo { get; set;  }
    }
}
namespace ClassLibrary1.Xml
{
    [CoverageExclude]
    public class SubDeco : BaseDeco, ISub
    {
        private readonly ISub _sub;
        
        public SubDeco(ISub @sub) : base(@sub)
        {
            _sub = @sub;
        }
        
        public SubDeco() : this(new Sub())
        {
        }
        
        public ISub Sub
        {
            get { return _sub; }
        }
        public int SubInt 
        {
            get { return _sub.SubInt; }
            set { _sub.SubInt = value; }
        }
        public string SubString 
        {
            get { return _sub.SubString; }
            set { _sub.SubString = value; }
        }
        IPartTwo ISub.PartTwo 
        { 
            get { return _sub.PartTwo; }
            set { _sub.PartTwo = value; }
        }
        public PartTwoDeco PartTwo 
        { 
            get { return new PartTwoDeco( _sub.PartTwo); }
            set { _sub.PartTwo = value.PartTwo; }
        }
        [System.Xml.Serialization.XmlIgnore]
        public bool IsLarge 
        {
            get { return _sub.IsLarge; }
        }
    }
}
 
#pragma warning restore 1591

Other Thoughts

I think the biggest issue here is the documentation. Writing it in the tt file as a string is error prone and unproductive. Possible solutions are:

  1. Define everything in an XML file. Better but still hard to write documentations.
  2. Leaving interfaces in regular cs file, let the tt template parse the interface source file and extract the information. Nice but hard to implement.

Both approach requires further investigation. Your thoughts?

The Template

Finally, here is the template that does all the dirty work.

// <autogenerated>
//   This file was generated from T4 template BvoGeneratorTemplate.tt.
// </autogenerated>
<#@ template language="C#v3.5" hostspecific="True" debug="True" #>
<#@ import namespace="System.Collections.Generic" #>
<#+
public class BvoGeneratorTemplate : Template
{
    public IEnumerable<Property> Properties;
    public string Name;
    public string Base;
    public string Document = string.Empty;
 
    public override string TransformText()
    {
#>
#pragma warning disable 1591
namespace ClassLibrary1
{
<#+
        foreach(string line in Document.Split(new []{'\r','\n'}, StringSplitOptions.RemoveEmptyEntries))
        {
#>
    /// <#=line#>
<#+
        }
#>
    public interface I<#=Name#><#=Base==null? "" : " : I" + Base#> 
    {
<#+
        foreach(Property property in Properties)
        {
            foreach(string line in property.Document.Split(new []{'\r','\n'}, StringSplitOptions.RemoveEmptyEntries))
            {
#>
        /// <#=line#>
<#+
            }
#>
        <#=property.Type#> <#=property.Name#> { <#=property.HasGetter ? "get; " : ""#><#=property.HasSetter? "set; " : ""#> }
<#+
        }
#>
    }
 
    public partial class <#=Name#> : <#=Base==null? "" : Base + ", "#>I<#=Name#>
    {
<#+
        foreach(Property property in Properties)
        {
            if(property.IsComputed) continue;
#>
        [CoverageExclude]
        public virtual <#=property.Type#> <#=property.Name#> { <#=property.HasGetter ? "get; " : ""#><#=property.HasSetter? "set; " : ""#> }
<#+
        }
#>
    }
}
<#+
        var pName = Name;
        if(pName.Length>1 && char.IsUpper(pName[0]) && ! char.IsUpper(pName[1]))
        {
            pName = char.ToLower(pName[0]) + pName.Substring(1);
        }
#>
namespace ClassLibrary1.Xml
{
    [CoverageExclude]
    public class <#=Name#>Deco : <#=Base==null? "" : Base + "Deco, "#>I<#=Name#>
    {
        private readonly I<#=Name#> _<#=pName#>;
        
        public <#=Name#>Deco(I<#=Name#> @<#=pName#>)<#=Base==null? "" : " : base(@"+pName+")"#>
        {
            _<#=pName#> = @<#=pName#>;
        }
        
        public <#=Name#>Deco() : this(new <#=Name#>())
        {
        }
        
        public I<#=Name#> <#=Name#>
        {
            get { return _<#=pName#>; }
        }
<#+
        foreach(Property property in Properties)
        {
            if (!property.IsBvo)
            {
                if(property.IsComputed) {
#>
        [System.Xml.Serialization.XmlIgnore]
<#+
                }
#>
        public <#=property.Type#> <#=property.Name#> 
        {
<#+
                if (property.HasGetter) {
#>
            get { return _<#=pName#>.<#=property.Name#>; }
<#+
                }
                if (property.HasSetter) {
#>
            set { _<#=pName#>.<#=property.Name#> = value; }
<#+
                }
#>
        }
<#+
            }
            else
            {
                var type = property.Type;
                if (type.Length>1 && type[0] == 'I' && char.IsUpper(type[1]))
                {
                    type = type.Substring(1);
                }
#>
        <#=property.Type#> I<#=Name#>.<#=property.Name#> 
        { 
            get { return _<#=pName#>.<#=property.Name#>; }
            set { _<#=pName#>.<#=property.Name#> = value; }
        }
        public <#=type#>Deco <#=property.Name#> 
        { 
            get { return new <#=type#>Deco( _<#=pName#>.<#=property.Name#>); }
            set { _<#=pName#>.<#=property.Name#> = value.<#=type#>; }
        }
<#+
            }
        }
#>
    }
}
 
#pragma warning restore 1591
<#+
        return this.GenerationEnvironment.ToString();
    }
}
    public class Property
    {
        public string Name;
        public string Type;
        public bool IsBvo;
        public bool IsComputed;
        public bool HasGetter = true;
        public bool HasSetter = true;
        public string Document = string.Empty;
    }
#>

No comments:

Post a Comment