Setting up a project with centralized model info using DataObjects.Net.

//Setting up a project with centralized model info using DataObjects.Net.

Setting up a project with centralized model info using DataObjects.Net.

In our project KLIB we use centralized model information. Our 5-tier application (View, ViewModel, Model, DataLayer, DB) uses T4 to generate the partial classes for the Model, ViewModel. The management of the DB Tables is done for us by DataObjects.Net. DataObjects.Net is capable of detecting model changes from the meta-data from the DB and the model in code.

We structured all our generation around the model-info which is based in the hand coded model classes.

Here’s a small part of our model for Contact. You can see the Person as part of it. The complexity of the Contact model lies in the fact that we have to be able to create a history of contacts (on which I will not elaborate here).

Contact

What you see below is our definition of the model in our own attributes at the start of the class (in this case the Person class called ‘Person.c’, note this definition is not complete because the Person class inherits the Contact class which is not described here, therefore there is no key in the description present.).

    #region EntityMembers
    [EntityClassAttribute]
    [EntityMemberAttribute(MemberName = "PersonnelNumber", Type = typeof(string))]
    [EntityMemberAttribute(MemberName = "Nickname", Type = typeof(string))]
    [EntityMemberAttribute(MemberName = "FirstNames", Type = typeof(string))]
    [EntityMemberAttribute(MemberName = "BirthDate", Type = typeof(Date))]
    [EntityMemberAttribute(MemberName = "DeathDate", Type = typeof(Date))]
    [EntityMemberAttribute(MemberName = "AHVNumberOld", Type = typeof(string))]
    [EntityMemberAttribute(MemberName = "AHVNumberNew", Type = typeof(string))]
    [EntityMemberAttribute(MemberName = "Title", Type = typeof(string))]
    [EntityMemberAttribute(MemberName = "IsUnknown", Type = typeof(bool))]
    [EntityMemberAttribute(MemberName = "WantsToStayAnonymous", Type = typeof(bool))]
    [EntityMemberAttribute(MemberName = "IsUser", Type = typeof(bool))]
    [EntityMemberAttribute(MemberName = "WindowsLogonName", Type = typeof(string))]
    [EntityMemberAttribute(MemberName = "UILanguage", Type = typeof(string))]
    [EntityMemberAttribute(MemberName = "CivilStatusList", Type = typeof(EntitySet<CivilStatus>), BackTo = "Person", IsOwned = true)]
    [EntityMemberAttribute(MemberName = "ToDocumentPersonAuthorList", Type = typeof(EntitySet<DossierDocument>), BackTo = "Author", IsOwned = true)]
    [EntityMemberAttribute(MemberName = "ToDocumentPersonSenderList", Type = typeof(EntitySet<DossierDocument>), BackTo = "Sender", IsOwned = true)]
    [EntityMemberAttribute(MemberName = "ToAuthorPersonList", Type = typeof(EntitySet<DossierMeeting>), BackTo = "Person", IsOwned = true)]
    [EntityMemberAttribute(MemberName = "DossierMeetingPersonList", Type = typeof(EntitySet<DossierMeetingPerson>), BackTo = "Person", IsOwned = true)]
    [EntityMemberAttribute(MemberName = "DossierTaskList", Type = typeof(EntitySet<DossierTask>), BackTo = "Originator", IsOwned = true)]
    [EntityMemberAttribute(MemberName = "Gender", Type = typeof(Gender), IsNullable = true)]
    [EntityMemberAttribute(MemberName = "LastNameList", Type = typeof(EntitySet<LastName>), BackTo = "Person", IsOwned = true)]
    [EntityMemberAttribute(MemberName = "NationalityList", Type = typeof(EntitySet<Nationality>), BackTo = "Person", IsOwned = true)]
    [EntityMemberAttribute(MemberName = "PersonDossierAppointmentList", Type = typeof(EntitySet<PersonDossierAppointment>), BackTo = "Person", IsOwned = true)]
    [EntityMemberAttribute(MemberName = "PersonDossierTaskList", Type = typeof(EntitySet<PersonDossierTask>), BackTo = "Person")]
    [EntityMemberAttribute(MemberName = "PersonEmploymentSituationList", Type = typeof(EntitySet<PersonEmploymentSituation>), BackTo = "Person", IsOwned = true)]
    #endregion
    [Serializable]
    public partial class Person : Contact, IDisposable, IComparable
    {

    }

Below is the definition of our so called AbstractEntity class from which all our model classes inherit. Here you see the key definition too (‘Id’) which results in a KeyGenerator being generated in code (see below).

    #region EntityMembers
    [EntityRootClassAttribute]
    [EntityMemberAttribute(MemberName = "Id", Type = typeof(int), IsKey = true, IsNullable = false)]
    [EntityMemberAttribute(MemberName = "CreatedOn", Type = typeof(DateTime), IsStamp = true, IsNullable = false)]

    // The CreatedBy and ChangedBy are added as ints. If we would add Person here, any Person ever having made a change
    // to the DB can not be deleted anymore (referential integrity).
    // Further would the construction be very costly from a performance point of view.
    [EntityMemberAttribute(MemberName = "CreatedBy", Type = typeof(int), IsStamp = true, IsNullable = false)]
    [EntityMemberAttribute(MemberName = "ChangedOn", Type = typeof(DateTime), IsStamp = true, IsNullable = false)]
    [EntityMemberAttribute(MemberName = "ChangedBy", Type = typeof(int), IsStamp = true, IsNullable = false)]
    [EntityMemberAttribute(MemberName = "IsDefault", Type = typeof(bool))]
    #endregion
    [Serializable]
    public abstract partial class AbstractEntity : Entity, INumberedObject, IValidatable, IDeletable
    {
    
    }

 

We wrote a library that uses the EnvDTE of Visual Studio to find and interpret the attributes in all the hand coded model classes. The library is used as an assembly in the T4 scripts. Here’s the start of our T4 script for our code model (our CodeModel assembly is call Diartis.KLIB.CodeModel).

<#@ template language="C#" hostSpecific="true" debug="true" #>
<#@ output extension="txt" #>
<#@ assembly name="EnvDTE" #>
<#@ assembly name="EnvDTE80" #>
<#@ assembly name="System.Windows.Forms" #>
<#@ assembly name="System.Core" #>
<#@ VolatileAssembly processor="T4Toolbox.VolatileAssemblyProcessor" name="%ProgramFiles(x86)%\DataObjects.Net\Bin\Latest\Xtensive.Storage.Rse.dll" #>
<#@ VolatileAssembly processor="T4Toolbox.VolatileAssemblyProcessor" name="%ProgramFiles(x86)%\DataObjects.Net\Bin\Latest\Xtensive.Integrity.dll" #>
<#@ VolatileAssembly processor="T4Toolbox.VolatileAssemblyProcessor" name="%ProgramFiles(x86)%\DataObjects.Net\Bin\Latest\Xtensive.Core.dll" #>
<#@ VolatileAssembly processor="T4Toolbox.VolatileAssemblyProcessor" name="%ProgramFiles(x86)%\DataObjects.Net\Bin\Latest\Xtensive.Storage.dll" #>
<#@ VolatileAssembly processor="T4Toolbox.VolatileAssemblyProcessor" name="$(SolutionDir)..\KLIBLibrary\bin\Debug\KLIBLibrary.dll" #>
<#@ VolatileAssembly processor="T4Toolbox.VolatileAssemblyProcessor" name="$(SolutionDir)KLIBCodeModel\bin\Debug\KLIBCodeModel.dll" #>
<#@ import namespace="EnvDTE" #>
<#@ import namespace="EnvDTE80" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Windows.Forms" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Diagnostics" #>
<#@ Import Namespace="Diartis.KLIB.CodeModel" #>
<#@ Import Namespace="Diartis.KLIB.CodeModel.CodeModelAttributes" #>
<#@ Import Namespace="Diartis.KLIB.KLIBLibrary" #>
<#@ Include file="FileOutputManager.ttinclude" #>
<#@ Include file="ModelHelper.tt" #>
<#
    try
    {
#>    #region Prepare code model
<#
        // START main try
        // Initialization Output Manager
        Manager outputManager = Manager.Create(Host, GenerationEnvironment);
#><#
        // Start Costruction CodeModelThree
        IServiceProvider hostServiceProvider = (IServiceProvider)Host;
        DTE2 dte = (EnvDTE80.DTE2)System.Runtime.InteropServices.Marshal.GetActiveObject("VisualStudio.DTE.10.0");

        string templateFileName = Path.ChangeExtension(Path.GetFileName(Host.TemplateFile), ".cs"); //this is the fileName of the generated cs

        string directoryPath = Host.ResolvePath("..\\..\\KLIBDatabase");
        DirectoryInfo dir = new DirectoryInfo(directoryPath); // in this path are all classes for that the DO/KLIB classes should be generated
        FileInfo[] files = dir.GetFiles("*.cs");
        CodeModelManager codeModelManager = new CodeModelManager();

        foreach (FileInfo file in files)
        {
            if(!file.Name.Equals(templateFileName)) //don't try to parse the output of this transformation
            {
                //dte = (EnvDTE80.DTE2)System.Runtime.InteropServices.Marshal.GetActiveObject("VisualStudio.DTE.10.0");
                ProjectItem inputProjectItem = dte.Solution.FindProjectItem(file.Name);  //try catch besser
                if (inputProjectItem != null)
                { // START inputProjectItem not null
                    FileCodeModel2 codeModel = (FileCodeModel2)inputProjectItem.FileCodeModel;
                    codeModelManager.AddClass(codeModel);
                } // END inputProjectItem not null

            }
        } // fileInfo foreach end

        List<CodeModelClass> classList = codeModelManager.Classes;
        // End Costruction CodeModelThree

        bool PreloadReferenceEntities = false;

#>        #endregion

As you can see we use the ‘VolatileAssemblyProcessor’ which comes from the T4ToolBox from Codeplex (http://t4toolbox.codeplex.com/). You don’t really need the T4ToolBox but it make life easier. After the generation process finishes, all assemblies that are in the script remain in memory and are locked on disk. This causes the build of the CodeModel classes to fail because the compiler can not replace them. You will have to restart Visual Studio each time you would like to generate the CodeModel library which is annoying to say the least.

The next step in T4 is to start generating the code using the input of the CodeModel library (meta data from our attributes).

<#        if (classList.Count > 0)
        { // START classList > 0
            foreach(CodeModelClass modelClass in classList.Where(c => c.IsOfTypeDBClass))
            { // START foreach CodeModelClass
                // START writing partial class file
                outputManager.StartNewFile(modelClass.Name + ".Generated.cs");
#>

// Generation of your code goes here...

<#             outputManager.EndBlock();
            outputManager.Process(true); // Write files to disk
        } // END classList > 0
    } // END main try

This is what, part of, the generated Person class looks like (called Person.generated.cs). The generated attributes are for DataObjects.Net.

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Company: Diartis AG
//     Website: www.diartis.ch
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using Diartis.KLIB.KLIBDatabase;
using Diartis.KLIB.KLIBLibrary;
using Diartis.KLIB.KLIBLibrary.Debugging;
using Diartis.KLIB.KLIBLibrary.Exceptions;
using Diartis.KLIB.KLIBLibrary.Interfaces;
using Diartis.KLIB.CodeModel.EntityAttributes;
using Xtensive.Storage;
using Xtensive.Storage.Configuration;
using Xtensive.Core.IoC;

namespace Diartis.KLIB.Model
{ // START class namespace
    public  partial class Person : Contact , IComparable
    { // START partial class
        #region DataObjectsFields

        [Field(Nullable = true), FieldMapping("PersonnelNumber")]
        public string Z_PersonnelNumber { get;  set; }

        [Field(Nullable = true), FieldMapping("Nickname")]
        public string Z_Nickname { get;  set; }

        [Field(Nullable = true), FieldMapping("FirstNames")]
        public string Z_FirstNames { get;  set; }

        [Field(Nullable = true), FieldMapping("BirthDate")]
        public DateTime? Z_BirthDate { get;  set; }

        [Field(Nullable = true), FieldMapping("DeathDate")]
        public DateTime? Z_DeathDate { get;  set; }

        [Field(Nullable = true), FieldMapping("AHVNumberOld")]
        public string Z_AHVNumberOld { get;  set; }

        [Field(Nullable = true), FieldMapping("AHVNumberNew")]
        public string Z_AHVNumberNew { get;  set; }

        [Field(Nullable = true), FieldMapping("Title")]
        public string Z_Title { get;  set; }

        [Field(Nullable = false), FieldMapping("IsUnknown")]
        public bool Z_IsUnknown { get;  set; }

        [Field(Nullable = false), FieldMapping("WantsToStayAnonymous")]
        public bool Z_WantsToStayAnonymous { get;  set; }

        [Field(Nullable = false), FieldMapping("IsUser")]
        public bool Z_IsUser { get;  set; }

        [Field(Nullable = true), FieldMapping("WindowsLogonName")]
        public string Z_WindowsLogonName { get;  set; }

        [Field(Nullable = true), FieldMapping("UILanguage")]
        public string Z_UILanguage { get;  set; }

        [Field(LazyLoad = true)]
        [Association(OnOwnerRemove = OnRemoveAction.Clear, OnTargetRemove = OnRemoveAction.Clear, PairTo = "Z_Person")]
        public EntitySet<CivilStatus> Z_CivilStatusList { get;  set; }

        [Field(LazyLoad = true)]
        [Association(OnOwnerRemove = OnRemoveAction.Clear, OnTargetRemove = OnRemoveAction.Clear, PairTo = "Z_Author")]
        public EntitySet<DossierDocument> Z_ToDocumentPersonAuthorList { get;  set; }

        [Field(LazyLoad = true)]
        [Association(OnOwnerRemove = OnRemoveAction.Clear, OnTargetRemove = OnRemoveAction.Clear, PairTo = "Z_Sender")]
        public EntitySet<DossierDocument> Z_ToDocumentPersonSenderList { get;  set; }

        [Field(LazyLoad = true)]
        [Association(OnOwnerRemove = OnRemoveAction.Clear, OnTargetRemove = OnRemoveAction.Clear, PairTo = "Z_Person")]
        public EntitySet<DossierMeeting> Z_ToAuthorPersonList { get;  set; }

        [Field(LazyLoad = true)]
        [Association(OnOwnerRemove = OnRemoveAction.Clear, OnTargetRemove = OnRemoveAction.Clear, PairTo = "Z_Person")]
        public EntitySet<DossierMeetingPerson> Z_DossierMeetingPersonList { get;  set; }

        [Field(LazyLoad = true)]
        [Association(OnOwnerRemove = OnRemoveAction.Clear, OnTargetRemove = OnRemoveAction.Clear, PairTo = "Z_Originator")]
        public EntitySet<DossierTask> Z_DossierTaskList { get;  set; }

        [Field(Nullable = true), FieldMapping("GenderId")]
        [Association(OnOwnerRemove = OnRemoveAction.Clear, OnTargetRemove = OnRemoveAction.Clear)]
        public Gender Z_Gender { get;  set; }

        [Field(LazyLoad = true)]
        [Association(OnOwnerRemove = OnRemoveAction.Clear, OnTargetRemove = OnRemoveAction.Clear, PairTo = "Z_Person")]
        public EntitySet<LastName> Z_LastNameList { get;  set; }

        [Field(LazyLoad = true)]
        [Association(OnOwnerRemove = OnRemoveAction.Clear, OnTargetRemove = OnRemoveAction.Clear, PairTo = "Z_Person")]
        public EntitySet<Nationality> Z_NationalityList { get;  set; }

        [Field(LazyLoad = true)]
        [Association(OnOwnerRemove = OnRemoveAction.Clear, OnTargetRemove = OnRemoveAction.Clear, PairTo = "Z_Person")]
        public EntitySet<PersonDossierAppointment> Z_PersonDossierAppointmentList { get;  set; }

        [Field(LazyLoad = true)]
        [Association(OnOwnerRemove = OnRemoveAction.Clear, OnTargetRemove = OnRemoveAction.Clear, PairTo = "Z_Person")]
        public EntitySet<PersonDossierTask> Z_PersonDossierTaskList { get;  set; }

        [Field(LazyLoad = true)]
        [Association(OnOwnerRemove = OnRemoveAction.Clear, OnTargetRemove = OnRemoveAction.Clear, PairTo = "Z_Person")]
        public EntitySet<PersonEmploymentSituation> Z_PersonEmploymentSituationList { get;  set; }

        #endregion //END region DataObjectsFields

    }// END partial class
}// END class namespace

As promised above the generated AbstractEntity class with the KeyGenerator for DataObjects.Net.

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Company: Diartis AG
//     Website: www.diartis.ch
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using Diartis.KLIB.KLIBDatabase;
using Diartis.KLIB.KLIBLibrary;
using Diartis.KLIB.KLIBLibrary.Debugging;
using Diartis.KLIB.KLIBLibrary.Exceptions;
using Diartis.KLIB.KLIBLibrary.Interfaces;
using Diartis.KLIB.CodeModel.EntityAttributes;
using Xtensive.Storage;
using Xtensive.Storage.Configuration;
using Xtensive.Core.IoC;

namespace Diartis.KLIB.Model
{ // START class namespace
    [Service(typeof(KeyGenerator), Name="AbstractEntity")]
    public class AbstractEntityKeyGenerator : CachingKeyGenerator<int>
    {
        [ServiceConstructor]
        public AbstractEntityKeyGenerator(DomainConfiguration configuration)
            : base(configuration)
        {
        }
    }
    [HierarchyRoot, KeyGenerator(Name="AbstractEntity")]
    public abstract partial class AbstractEntity : Entity 
    { // START partial class
        #region DataObjectsFields

        [Key]
        [Field(Nullable = false), FieldMapping("Id")]
        public int Z_Id { get; private set; }

        [Field(Nullable = false), FieldMapping("CreatedOn")]
        public DateTime Z_CreatedOn { get;  set; }

        [Field(Nullable = false), FieldMapping("CreatedBy")]
        public int Z_CreatedBy { get;  set; }

        [Field(Nullable = false), FieldMapping("ChangedOn")]
        public DateTime Z_ChangedOn { get;  set; }

        [Field(Nullable = false), FieldMapping("ChangedBy")]
        public int Z_ChangedBy { get;  set; }

        [Field(Nullable = false), FieldMapping("IsDefault")]
        public bool Z_IsDefault { get;  set; }

        #endregion //END region DataObjectsFields

    }// END partial class
}// END class namespace

 

I have left out the code for our project since it will make no sense to publish that because it is very specific. You’ll have you own needs in your own project. What you see being generated here is all the attributes needed for DataObjects.Net to be able to create/upgrade the DB. All our properties are generated with the ‘Z_’ prefix. In our specific code we use a new property without this prefix as our model-properties. Here is what is generated for a, lazy loaded, property (Personnel number). The ModelProperty is a class that reacts to changes automatically. In our ViewModel we can bind to this property causing changes in the model to be automatically forwarded.

        protected ModelProperty<string> m_PersonnelNumberP;

        /// <summary>
        /// Gets the PersonnelNumber property. 
        /// </summary>
        // [DebuggerNonUserCode]
        public ModelProperty<string> PersonnelNumberP
        { // START KLIBModel property
            get
            { // START get KLIBModel property
                if (m_PersonnelNumberP == null)
                { // START lazy private property initialization
                    m_PersonnelNumberP = new ModelProperty<string>("PersonnelNumberP", Strings.ToNonNullString(Z_PersonnelNumber), true);
                    UpdateAttributes();
                    m_PersonnelNumberP.ValueChanged += PersonnelNumberPChanged;
                } // END lazy private property initialization
                return m_PersonnelNumberP;
            } // END get KLIBModel property
        } // END KLIBModel property

 

Because we generate partial classes we can extend the model classes by programming in the hand coded class (first example above). Specific code for Person goes into this class and is not overwritten by the generation process.

The ViewModel is built up the same way. We generate code there too.

What this causes is the following:

When we want to change something in the model we only need to do the following:

  • Change the attributes in the hand coded model class.
  • Regenerate the Model and ViewModel with 1 click in Visual Studio.
  • Rebuld the solution.
  • Start the application with ‘Perform’ mode for DataObjects.Net which automatically changes the model in the DB for us (it gets a bit more complex when data has to be manipulated during upgrade, but DataObjects.Net helps us here too with the so called ‘UpgradeHandler’).

That is it.

When we for instance have to add a new entity to the model this is done in minutes. All the structures are immediately available for further programming and the DB is automatically changed by DataObjects.Net.

After the generation process finishes, this is what the Project Explorer looks like in Visual Studio. The T4 script is called ‘Model.tt’:

image

I hope I gave you some insight in how to use code generation in collaboration with DataObjects.Net to create an easily maintained model in your projects.

By |2016-01-24T17:14:10+00:00March 12th, 2011|.Net Development|0 Comments

About the Author:

Leave A Comment