En estos días, estuve trabajando en generar código de clases C#, usando como punto de partida los archivos .hbm, que se usan en NHibernate para especificar el mapeo de clases y tablas de bases relacionales. Como es usual, cuando encaro algo de generación de código uso AjGenesis, mi proyecto open source de generación de código (practico el “dog fooding” :-).
(English version of this post Generating code with AjGenesis using NHibernate hbm files)
Pueden bajar un ejemplo de lo que estoy haciendo de mi Skydrive:
Examples > AjGenesis > NHibernateMappingExample01.zip
(el código en desarrollo está en el trunk, en este change actual, en el directorio examples\NHibernateMappinp:
pero si quieren ir directamente al ejemplo, pueden bajárselo completo desde el Skydrive que mencioné, que incluye el ejecutable de AjGenesis de la versión en desarrollo, no necesitan compilar nada).
Luego de expandir el archivo del ejemplo, tendrán este contenido:
Para crear clases C#, pueden probar de ejecutar los comandos:
GenerateClasses AjFirstExample GenerateClasses AjTestPara crear un proyecto .NET con los archivos .cs y .hbm, y una solución, ejecutar:
GenerateProject AjFirstExample GenerateProject AjTestLos archivos generados, en ambos casos, quedan en el directorio Build.
Hay dos proyectos ejemplo que son AjFirstExample, con dos mapeos simples, y AjTest, que tiene mapeos más interesantes, con “bags” y relaciones “many to one”.
En el ejemplo, cada proyecto se describe con un simple archivo Project.xml:
<Project Name="AjTest"> </Project>En cuanto necesite más información, lo agregaré ahí, o en tags meta de los propios archivos de mapeo y configuración.
Este es uno de los archivos de mapeo que sirven de modelo inicial para este ejemplo de generación, en Projects\AjTest\Mappings, Department.hbm:
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="AjTest.Entities" namespace="AjTest.Entities" > <class name="Department" table="departments"> <id name="Id" column="Id" type="Int32"> <generator class="native"/> </id> <property name="Description" type="String"/> <bag name="Employees" lazy="true" inverse="true" cascade="all"><key column="IdDepartment"/> <one-to-many class="AjTest.Entities.Employee, AjTest.Entities"/> </bag> </class> </hibernate-mapping>Este es el código generado para este mapeo, Department.generated.cs:
using System; using System.Collections.Generic; using Iesi.Collections.Generic; namespace AjTest.Entities { public class Department { public int Id { get; set; } public string Description { get; set; } public IList<Employee> Employees { get; set; } public Department() { this.Employees = new List<Employee>(); } } }Veamos el proceso de generación. Este es el contenido de GenerateProject.cmd:
@echo off set ProjectName=%1% if "%1%"=="" set ProjectName=AjFirstExample Bin\AjGenesis.Console.exe Projects\%ProjectName%\Project.xml Tasks\AddMappings.ajg Tasks\BuildCSharp.ajg xcopy Libraries\*.* Build\%ProjectName%\CSharp\Src\Libraries /Y /QLa línea más importante es la que invoca a AjGenesis.Console.exe. El contenido de Project.xml se carga en memoria. La tarea AddMapping.ajg se carga y ejecuta (está escrita en un lenguaje dinámico, afectuosamente llamado AjBasic), y luego, se procesa la tarea BuildCSharp.ajg. Veamos el código de AddMapping.ajg:
' Add mappings from directory if not specified in Project model Include("Utilities/Utilities.tpl") if not Project.Mappings then Project.Mappings = CreateList() di = new System.IO.DirectoryInfo("Projects/${Project.Name}/Mappings") for each fi in di.GetFiles("*.hbm.xml") filename = fi.Name Project.Mappings.Add(filename.Substring(0, filename.Length - 8)) end for end ifEncuentra y agrega los nombres de los archivos de mapeo contenidos en el directorio de Mapping, dentro del proyecto (noten que se pueden usar clases y objetos del framework .NET). Una tarea más interesante es la GenerateCSharp.ajg. Primero, carga la dll de NHibernate, para usar más adelante su parser de archivos hbm:
include "Utilities/Utilities.tpl" include "Utilities/FileUtilities.tpl" include "Utilities/TypeUtilities.tpl" Include("Utilities/NHibernateUtilities.tpl") include "Templates/CSharp/UtilitiesCs.tpl" include "Templates/CSharp/CSharpFunctions.tpl" AssemblyManager.LoadFrom("Libraries/NHibernate.dll") parser = new NHibernate.Cfg.MappingSchema.MappingDocumentParser()Crea objetos dinámicos, donde coloca información de la solución y el proyecto a crear:
if not Project.BuildDir then Project.BuildDir = "Build/${Project.Name}/CSharp" end if message "Creating Directories..." FileManager.CreateDirectory(Project.BuildDir) FileManager.CreateDirectory("${Project.BuildDir}/Sql") FileManager.CreateDirectory("${Project.BuildDir}/Src") FileManager.CreateDirectory("${Project.BuildDir}/Src/Libraries") message "Defining Solution and Projects..." Project.Solution = CreateObject() Project.Solution.Guid = "FAE04EC0-301F-11D3-BF4B-00C04F79EFBC" Project.Solution.Projects = CreateList() message "Defining Entities Project..." PrjEntities = CreateObject() PrjEntities.Includes = CreateList() PrjEntities.Guid = CreateGuid() PrjEntities.COMGuid = CreateGuid() Project.Solution.Projects.Add(PrjEntities) Project.Entities = CreateList()Ahora, itera sobre cada archivo hbm, lo lee usando el parser del propio NHibernate, toma información sobre las clases a generar:
for each MappingName in Project.Mappings filename = "Projects/${Project.Name}/Mappings/${MappingName}.hbm.xml" mapping = parser.Parse(OpenAsStream(filename)) for each hbmclass in mapping.Items where IsType(hbmclass, "HbmClass") Entity = CreateObject() Project.Entities.Add(Entity) Entity.ClassName = hbmclass.name Entity.Namespace = mapping.namespace ' Namespace as default project name for Entities Project if not PrjEntities.Name then PrjEntities.Name = mapping.namespace PrjEntities.Directory = "${Project.BuildDir}/Src/${PrjEntities.Name}" FileManager.CreateDirectory(PrjEntities.Directory) end if Entity.Properties = CreateList() if hbmclass.Id then Property = CreateObject() Property.Name = hbmclass.Id.name Property.Type = HbmTypeToCSharp(hbmclass.Id.type1, Entity.Namespace) Entity.Properties.Add(Property) end if for each item in hbmclass.Items if IsType(item, "HbmProperty") then Property = CreateObject() Property.Name = item.name Property.Type = HbmTypeToCSharp(item.type1, Entity.Namespace) Entity.Properties.Add(Property) end if if IsType(item, "HbmManyToOne") then Property = CreateObject() Property.Name = item.name Property.Type = HbmTypeToCSharp(item.class, Entity.Namespace) Entity.Properties.Add(Property) end if if IsType(item, "HbmSet") then Property = CreateObject() Property.Name = item.name Property.IsSet = true Property.Type = HbmTypeToCSharp(item.Item.class, Entity.Namespace) Entity.Properties.Add(Property) end if if IsType(item, "HbmBag") then Property = CreateObject() Property.Name = item.name Property.IsList = true Property.Type = HbmTypeToCSharp(item.Item.class, Entity.Namespace) Entity.Properties.Add(Property) end if end for end for end forPueden extender esta capacidades, procesando más tags (debería escribir un ejemplo usando los tags meta que puede contener el hbm; ya utilitarios de Java, como el venerable XDocLet, usaban esos tags para ayudarse en la generación de código, en Hibernate), y detectar más formas de mapeo de NHibernate. Ahora, pasa a la generación de código:
for each Entity in Project.Entities TransformerManager.Transform("Templates/CSharp/Entity.tpl", "${PrjEntities.Directory}/${Entity.ClassName}.generated.cs", Environment) PrjEntities.Includes.Add(CreateFileCs("${Entity.ClassName}.generated")) end forLa tarea genera los archivos .cs, y también crea un archivo de solución y otro de proyecto C#, copiando y embebiendo los archivos de mapeo originales:
for each MappingName in Project.Mappings filename = "Projects/${Project.Name}/Mappings/${MappingName}.hbm.xml" targetfilename = "${PrjEntities.Directory}/${MappingName}.hbm.xml" System.IO.File.Copy(filename, targetfilename, true) PrjEntities.Includes.Add(CreateFileType(MappingName,"hbm.xml")) end for for each CsProject in Project.Solution.Projects where CsProject.ProjectType<>"Web" FileManager.CreateDirectory(CsProject.Directory) FileManager.CreateDirectory(CsProject.Directory & "/Properties") TransformerManager.Transform("Templates/CSharp/CsProject.tpl", "${CsProject.Directory}/${CsProject.Name}.csproj", Environment) TransformerManager.Transform("Templates/CSharp/AssemblyInfoCs.tpl", "${CsProject.Directory}/Properties/AssemblyInfo.cs", Environment) end for TransformerManager.Transform("Templates/Solution.tpl", "${Project.BuildDir}/Src/${Project.Name}.sln", Environment)Esta es la solución generada:
Próximos pasos
Debería trabajar en algunos puntos:
– Generar una solución más completa, como en otros ejemplos de AjGenesis (con un proyecto de infraestructura, un proyecto de Service Layer o similar, una presentación web, etc…).
– Soportar más opciones de mapeo de NHibernate.
– Usar los tags meta.
Por ahora, pueden jugar con este ejemplo. Pueden cambiar los templates para generar más artefactos, por ejemplo, código VB.NET.
Gracias a @fabiomaulo por avisarmr de las capacidades públicas de parser de hbm dentro de NHibernate!
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com