DataObjects.Net code T4 generation

///DataObjects.Net code T4 generation

DataObjects.Net code T4 generation

DataObjects.Net is a very versatile Object Relational Mapper (ORM). What an ORM does is solve the relation object-oriented clash we developers always have when we program in OO languages like C#, Java, C++, etc in combination with a Relational Database like f.i. Microsoft SQL Server. After careful deliberation we decided to use DataObjects.Net (DO) as our ORM.

We also wanted to use T4 Code Generation for our ViewModel and extension of the Data Model. Now DO does not support T4 out of the box. In a previous project we wrote a library that uses reflection to dissect the compiled Assembly produced by the compiler in combination with Postsharp (used by DO to inject code).

Now in DO, which is primarily a ‘code first’ tool, you define your Data Model in code using Attribute classes. Here’s an example.

using Xtensive.Orm;

namespace T4DB.Entities
{
    [HierarchyRoot, KeyGenerator(Name = "Root")]
    [TableMapping("Root")]
    public class RootEntity : Entity
    {
        [Key]
        [Field(Nullable = false)]
        public int Id { get; private set; }
    }
}

using Xtensive.Orm;

namespace T4DB.Entities
{
    [TableMapping("Person")]
    public class PersonEntity : RootEntity
    {
        [Field(Nullable = false)]
        public string FirstName { get; set; }

        [Field(Nullable = false)]
        public string LastName { get; set; }

        [Field(Nullable = false)]
        public string Prefix { get; set; }
    }
}

As you can see we use a RootEntity that is inherited by, in this case, the PersonEntity. We use this construct thoughout the DataModel. What this causes is that each entity has its own unique key throughout all the data (no row in a table has the same key). This is a unique feature of DO and gives us the ability to find entities just by its key. DO takes care of the resolution into the right type for us. Ok so far this little side-step.

After compiling the above Data Model you get an Assembly. An Assembly is a written into a DLL stored on disk. In the case above the name will be ‘My.Model.dll’.

What is present in this Assembly we are able to dissect using reflection. Now we could reflect all the information we need in T4 but that would clutter the templates so we decided to build a second Assembly that is going to gather all the needed information for code generation.

Here’s an example showing the loading of the Data Model Assembly into a Metadata instance in a T4 template.

<#@ output extension=".cs" #>
<#@ Include file="$(SolutionDir)Common.T4\Assemblies.ttinclude" #><#   
    IServiceProvider hostServiceProvider = (IServiceProvider)Host;

    string directoryPath = Host.ResolvePath(@"..\..\My.Model");
    string configName = dte.Solution.SolutionBuild.ActiveConfiguration.Name;
    string assemblyName = directoryPath + @"\bin\Debug\My.Model.dll";

    Assembly assembly = Assembly.LoadFrom(assemblyName);
    Metadata.MetadataContainer.AddAssembly(assembly);
#>

Now of cource we need the Metadata class. Here is the part of that class that loads the assembly from disk into the MetadataContainer using the AddAssembly() method.

using System;
using System.Linq;
using System.Collections.Generic;
using Xtensive.Orm;
using System.Reflection;
using System.Windows.Forms;

namespace My.Metadata
{
    /// <summary>
    /// Metadata container class.
    /// </summary>
    public class Metadata
    {
        private static Metadata m_MetadataContainer;

        /// <summary>
        /// Singleton for obtaining a MetadataContainer object
        /// </summary>
        public static Metadata MetadataContainer
        {
            get
            {
                if (m_MetadataContainer == null)
                {
                    m_MetadataContainer = new Metadata();
                }

                return m_MetadataContainer;
            }
        }
        
        /// <summary>
        /// Add all DO.Net Entity types of an Assembly
        /// </summary>
        public void AddAssembly(Assembly assembly)
        {
            try
            {
                foreach (var entity in assembly.GetTypes())
                {
                    AddEntity(entity);
                }
            }
            catch (ReflectionTypeLoadException lex)
            {
                int hr = System.Runtime.InteropServices.Marshal.GetHRForException(lex);
                MessageBox.Show(lex.LoaderExceptions[0].Message);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

        private List<EntityMetadata> m_Entities;

        /// <summary>
        /// List of Entities in the metadata collection.
        /// </summary>
        public List<EntityMetadata> Entities
        {
            get
            {
                if (m_Entities == null)
                {
                    m_Entities = new List<EntityMetadata>();
                }

                return m_Entities;
            }
        }

        /// <summary>
        /// Add en entity type to the container.
        /// </summary>
        private void AddEntity(Type entity)
        {
            if (!ContainsEntity(entity))
            {
                if (entity.IsSubclassOf(typeof(Entity)))
                {
                    Entities.Add(new EntityMetadata(entity));
                }
            }
        }

        /// <summary>
        /// Is the type already present in the container?
        /// </summary>
        private bool ContainsEntity(Type type)
        {
            foreach (EntityMetadata entityWrapper in Entities)
            {
                if (entityWrapper.Type == type)
                {
                    return true;
                }
            }

            return false;
        }
    }
}

Note: This class may not compile correctly because I combined several snippets from the original code. You can find the complete Metadata class in the attached zip file.

In the AddAssembly() you can see we add all DO entities (the AddEntity adds type Entity only) using the EntityMetadata class.

After loading the Assembly in the Metadata object we have all the DataObject Entities in a collection. Now each of the above mentioned EntityMetadata collects all the member information from the Entity. So after loading the Assembly we have an Entities collection and per Entity a Properties collection through which we can iterate in our template. Below is the MyFirstTemplate.tt taken from the attached zip file.

<#@ output extension=".txt" #>
<#@ Include file="$(SolutionDir)T4Includes\Assemblies.ttinclude" #><#   
    try
    {    // START main try
        // Initialization Output Manager
        Manager outputManager = Manager.Create(Host, GenerationEnvironment);

        // Start Costruction CodeModelTree
        IServiceProvider hostServiceProvider = (IServiceProvider)Host;
        DTE2 dte = (EnvDTE80.DTE2)System.Runtime.InteropServices.Marshal.GetActiveObject("VisualStudio.DTE.11.0");

        string directoryPath = Host.ResolvePath(@"..\My.Model");
        string assemblyName = directoryPath + @"\bin\Debug\My.Model.dll";
        Assembly assembly = Assembly.LoadFrom(assemblyName);
        Metadata.MetadataContainer.AddAssembly(assembly);
#>
// Empty file
<#
        try
        {
            foreach (EntityMetadata entity in Metadata.MetadataContainer.Entities)
            {
                if(entity != null)
                {
                    outputManager.StartNewFile(entity.VMName + ".generated.cs");

#>
using System;

namespace MyNamespace
{
    public partial class <#= entity.Name #>
    {
        public <#= entity.Name #>()
        {
        }
<#
                    foreach (var property in entity.Properties)
                    {
#>
        public <#= property.FieldTypeString #> <#= property.Name #> { get; set; }
<#
                    }
                }
#>
    }
}
<#
            }
        }
        catch (Exception ex)
        {
#>
            // ERROR <#= ex.Message #> 
            // STACK <#= ex.StackTrace #>
<#
        }

        outputManager.Process(true); //write files to disk

    } // END main try
    catch(Exception ex)
    {
        MessageBox.Show("Error in MyFirstTemplate.tt: " + Environment.NewLine + Environment.NewLine + ex.ToString(),"Error in Transformation");
    }
#>
<#+  
#>

After saving this file to disk the template generator will start loading the template and saves the generated output to disk and adds it to the project as subitems from the template.

image

In for instance the VMPersonEntity.generated.cs the following is generated with this template.

using System;

namespace MyNamespace
{
    public partial class PersonEntity
    {
        public PersonEntity()
        {
        }
        public String FirstName { get; set; }
        public String LastName { get; set; }
        public String Prefix { get; set; }
    }
}

Solution: T4.zip

PS: In order to be able to compile this solution you will need to add the DataObjects.Net Nuget package via the Nuget Package Manager.

image

Type ‘Dataobjects’ in the searchbox in the upper right corner and wait for the result.

Click the ‘Install’ button.

image

Select the following projects for installation.

image

After that start the compilation. It is possible the version of DO and PostSharp is different in your new installation. In that case you will have to change the version info in the ‘Assemblies.ttinclude’ file in the T4Include project.

Also remove the RequiresPostSharp.cs files where not needed. That file is only needed in the T4Database project.

By |2016-01-24T17:06:10+00:00May 4th, 2013|.Net Development, T4|0 Comments

About the Author:

Leave A Comment