Wednesday, January 13, 2010

T4 Generates my business objects for me

I have finally started understanding/applying Oleg Synch's T4 code generation tutorials. I'm at least to the point where I've made something useful. My goal was to design a business object generator that would be reusable between persistence and ui changes (actual code changes, or swapping out to a completely different layer) without recompiling, or making any changes what-so-ever in the business module. It consists of a Template that defines what a business object class should look like, and related scripts for particular business object details.

The main difficulties were:
  • validation 
    • centralized validation model
    • being capable of providing multiple field specific error messages, instead of just the first one that is discovered
  • mapping objects
    • looking for a way to cope with Linq-To-Sql's entities, while maintaining persistance indepdendence
What remains:
  • Validation
    • Investigate ways to push validation logic out to the client-side
  • Decide on an extensibility option
    • Partial classes
      • Would this allow someone to implement additional functionality in another assembly? That would be against the design.
    • Inheritance
      • Everyone nowadays is avoiding inheritance in favor of interfaces
    • Direct Script (not the generic template, the script specific to a business object type) modification 
    • Make the class generic so it can hold another object for additional functionality
The template and script are included.

The template BusinessObjectTemplate.tt:

<#@ assembly name="System.Core" #>
<#@
import namespace="System.Collections.Generic" #>
<#@
Import Namespace="System.Linq" #>
<#+


    public class BusinessPropertyT4
    {
        private readonly string _typeString;
        public string TypeString { get { return _typeString; } }
      
        public BusinessPropertyT4(string type )
        {
            _typeString = type;
        }
        public string BoolValidation { get; set; }
        public string RuleViolationMessage { get; set; }
        public bool CopyExclude {get; set;}
    }
public class BusinessObjectTemplate : Template
{
    public string BusinessName;
  
    public IDictionary PropertyList;  
    public override string TransformText()
    {
#>using System;
using
System.Collections.Generic;
using
BReusable;
namespace
DefectSeverityAssessmentBusiness.<#=BusinessName #>
{
    public class Model<#=BusinessName #>:I<#=BusinessName #>
    {
  
        
#region I<#=BusinessName #> Members
        <#+
        PushIndent("\t");
        PushIndent("\t");
        foreach (string item in PropertyList.Keys)
        {
            WriteLine("public "+PropertyList[item].TypeString +" "+ item+" { get; set; }");
        }
        PopIndent();
        PopIndent();
    
#>  
        
#endregion
  
        
public IEnumerable GetRuleViolations()
        {
        <#+
        PushIndent("\t");
        PushIndent("\t");
        foreach (string item in PropertyList.Keys)
        {
            if(string.IsNullOrEmpty(PropertyList[item].BoolValidation)==false
                && string.IsNullOrEmpty(PropertyList[item].RuleViolationMessage)==false)
                {
            WriteLine("\tif ("+PropertyList[item].BoolValidation+")");
            WriteLine("\t
\t yield return new RuleViolation(\""+PropertyList[item].RuleViolationMessage
                +"\",Member.Name(x=>x."+item+"));");
                }
        }
        PopIndent();
        PopIndent();
    
#>

            //if (Latitude == 0 || Longitude == 0)
            //    yield return new RuleViolation("Make sure to enter a valid address!", "Address");

            yield break;
        }
        ///
        /// This is validated in a unit test to ensure accuracy and that it is not out of sync with
        
/// the number of members the interface has
        ///
        /// name="T">
        ///
name="dest">
        ///
name="source">
        ///
name="includeIdentifier">
        ///
        public static Dictionary Action> GenerateActionDictionary(T dest, I<#=BusinessName #> source, bool includeIdentifier)
    where T : I<#=BusinessName #>
        {
            var result = new Dictionary Action>
                {
<#+
        PushIndent("\t");
        PushIndent("\t");
        PushIndent("\t");
        PushIndent("\t");
        foreach (string item in PropertyList.Keys)
        {
            if(PropertyList[item].CopyExclude==false)
            {
            WriteLine("\t{Member.Name(x=>x."+item+"),");
            WriteLine("\t
\t()=>dest."+item+"=source."+item+"},");
            }
        }
        PopIndent();
        PopIndent();
        PopIndent();
        PopIndent();
    
#>
  
                
};

            return result;

        }

        ///
        /// Designed for copying the model to the db persistence object or ui display object
        ///
        /// name="T">
        ///
name="creator">
        ///
name="source">
        ///
name="includeIdentifier">
        ///
name="excludeList">
        ///
        public static T CopyData(Func creator, I<#=BusinessName #> source, bool includeIdentifier,
            ICollection excludeList) where T : I<#=BusinessName #>
        {
            return CopyDictionary I<#=BusinessName #>>.CopyData(
                GenerateActionDictionary, creator, source, includeIdentifier, excludeList);
        }
        ///
        /// Designed for copying the ui to the model
        
///
        /// name="T">
        ///
name="validation">
        ///
name="creator">
        ///
name="source">
        ///
name="includeIdentifier">
        ///
name="excludeList">
        ///
        public static T CopyData(IValidationDictionary validation, Func creator,
            I<#=BusinessName #> source, bool includeIdentifier, ICollection excludeList)
             where T : I<#=BusinessName #>
        {
            return CopyDictionary I<#=BusinessName #>>.CopyData(
                GenerateActionDictionary, validation, creator, source, includeIdentifier, excludeList);
        }

    } // end class
    public interface I<#=BusinessName #>
    {  
<#+
        PushIndent("\t");
        PushIndent("\t");
        foreach (string item in PropertyList.Keys)
        {
            WriteLine(PropertyList[item].TypeString+" "+item+" { get; set; }");
        }
        PopIndent();
        PopIndent();
    
#>
    }
}//end
namespace
        <#+
        PopIndent();
        return this.GenerationEnvironment.ToString();
    }
}
#>


The first script ModelRegistrationTemplate.tt

<#@ template language="C#v3.5" hostspecific="True" debug="True" #>
<#@
output extension="cs" #>
<#@
include file="T4Toolbox.tt" #>
<#@
include file="../BusinessObjectTemplate.tt" #>
<#

    BusinessObjectTemplate template = new BusinessObjectTemplate();
    template.BusinessName="Registration";
  
    template.PropertyList=new Dictionary{
        {"UserName",new BusinessPropertyT4("string"){BoolValidation="String.IsNullOrEmpty(UserName)",
            RuleViolationMessage="UserName is required"/*,CopyExclude=true */}},
        {"Name",new BusinessPropertyT4("string")},
        {"Email",new BusinessPropertyT4("string")},
        {"MailCode",new BusinessPropertyT4("string")},
        {"TelephoneNumber",new BusinessPropertyT4("string")},
        {"OrganizationId",new BusinessPropertyT4("int?")},
        {"OrganizationSponsorId",new BusinessPropertyT4("int?")},
    };
    template.Render();
#>



And the final output:



using System;
using System.Collections.Generic;
using BReusable;
namespace DefectSeverityAssessmentBusiness.Registration
{
    
public class ModelRegistration:IRegistration
    {
  
        #region IRegistration Members
        
public string UserName { get; set; }
        
public string Name { get; set; }
        
public string Email { get; set; }
        
public string MailCode { get; set; }
        
public string TelephoneNumber { get; set; }
        
public int? OrganizationId { get; set; }
        
public int? OrganizationSponsorId { get; set; }
  
        #endregion
  
        
public IEnumerable<RuleViolation> GetRuleViolations()
        {
            
if (String.IsNullOrEmpty(UserName))
                
yield return new RuleViolation("UserName is required",Member.Name<ModelRegistration>(x=>x.UserName));

            
//if (Latitude == 0 || Longitude == 0)
            //    yield return new RuleViolation("Make sure to enter a valid address!", "Address");

            yield break;
        }
        
///
        /// This is validated in a unit test to ensure accuracy and that it is not out of sync with
        /// the number of members the interface has
        ///
        ///
        ///

        ///

        ///

        ///
        public static Dictionary<string, Action> GenerateActionDictionary(T dest, IRegistration source, bool includeIdentifier)
    
where T : IRegistration
        {
            
var result = new Dictionary<string, Action>
                {
                    {
Member.Name<IRegistration>(x=>x.UserName),
                        ()=>dest.UserName=source.UserName},
                    {
Member.Name<IRegistration>(x=>x.Name),
                        ()=>dest.Name=source.Name},
                    {
Member.Name<IRegistration>(x=>x.Email),
                        ()=>dest.Email=source.Email},
                    {
Member.Name<IRegistration>(x=>x.MailCode),
                        ()=>dest.MailCode=source.MailCode},
                    {
Member.Name<IRegistration>(x=>x.TelephoneNumber),
                        ()=>dest.TelephoneNumber=source.TelephoneNumber},
                    {
Member.Name<IRegistration>(x=>x.OrganizationId),
                        ()=>dest.OrganizationId=source.OrganizationId},
                    {
Member.Name<IRegistration>(x=>x.OrganizationSponsorId),
                        ()=>dest.OrganizationSponsorId=source.OrganizationSponsorId},
  
                };

            
return result;

        }

        
///
        /// Designed for copying the model to the db persistence object or ui display object
        ///
        ///
        ///

        ///

        ///

        ///

        ///
        public static T CopyData(Func creator, IRegistration source, bool includeIdentifier,
            
ICollection<string> excludeList) where T : IRegistration
        {
            
return CopyDictionaryIRegistration>.CopyData(
                GenerateActionDictionary, creator, source, includeIdentifier, excludeList);
        }
        
///
        /// Designed for copying the ui to the model
        ///
        ///
        ///

        ///

        ///

        ///

        ///

        ///
        public static T CopyData(IValidationDictionary validation, Func creator,
            
IRegistration source, bool includeIdentifier, ICollection<string> excludeList)
            
where T : IRegistration
        {
            
return CopyDictionaryIRegistration>.CopyData(
                GenerateActionDictionary, validation, creator, source, includeIdentifier, excludeList);
        }

    }
// end class
    public interface IRegistration
    {  
        
string UserName { get; set; }
        
string Name { get; set; }
        
string Email { get; set; }
        
string MailCode { get; set; }
        
string TelephoneNumber { get; set; }
        
int? OrganizationId { get; set; }
        
int? OrganizationSponsorId { get; set; }
    }
}
//end namespace
        
The CopyDictionary class is as follows:


using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;

namespace DefectSeverityAssessmentBusiness
{
    
internal static class CopyDictionary where T:TU
    {
        
///
        ///
        ///
        ///

        ///

        ///

        ///

        ///
null for no exclusions
        ///
        public static T CopyData(Funcbool, Dictionary<string, Action>> actionDictionaryFunc,
            
Func creator, TU source, bool includeIdentifier,ICollection<string>excludeList)  
        {
            
return CopyDataMaster(actionDictionaryFunc, creator, source, includeIdentifier,excludeList, kvp => kvp.Value());
        }

        
///
        /// Attempts to copy data, stops on first error, adds to validation dictionary, throws the exception
        ///
        ///

        ///

        ///

        ///

        ///

        ///
null for no exclusions
        ///
        public static T CopyData(Funcbool, Dictionary<string, Action>> actionDictionaryFunc,
            
IValidationDictionary validation, Func creator,
            TU source,
bool includeIdentifier,ICollection<string> excludeList)
        {
            
var result= CopyDataMaster(actionDictionaryFunc, creator, source, includeIdentifier,excludeList,
                kvp =>
                    {
                        
try
                        {
                            kvp.Value();
                        }
                        
catch (Exception exception)
                        {
                            validation.AddError(kvp.Key,exception.Message);
                            
//TODO: log error?
                        }
                    });
            
if(validation.IsValid==false)
                
throw new ValidationException("Validation contains errors");
            
return result;
        }

        
private static T CopyDataMaster(Funcbool, Dictionary<string, Action>> actionDictionaryFunc,
            
Func creator,TU source, bool includeIdentifier,ICollection<string> excludeList,
            
Action<KeyValuePair<string, Action>> action)
        {
            
var result = creator();
            
foreach (var kvp in actionDictionaryFunc(result,source,includeIdentifier))
            {
                
if(excludeList==null || excludeList.Contains(kvp.Key)==false)
                action(kvp);
            }
          
            
return result;
        }

    }
}






1 comment: