Pragmatic Developer

Ali Özgür

Bookmark Blog

Add to Technorati Favorites

Google Talk

Chat with Ali Özgür

Purchase PragmaSQL from

Calendar

«  December 2008  »
MoTuWeThFrSaSu
24252627282930
1234567
891011121314
15161718192021
22232425262728
2930311234
View posts in large calendar

Tag Cloud

Don't show

    Authors

    Recent Comments

    Banners




    aliozgur posted on February 18, 2008 16:24

    Introduction

    Cuyahoga framework has very nice approach to Web development. It has bunch of built-in modules and you can develop your own modules in a
    couple of hours. If you have some experience with NHibernate and/or some other web framework your module development
    may even take less than an hour. My product site PragmaSQL Online runs on top of Cuyahoga framework and it took
    me just a couple of hours to bring this site up and running. Although Cuyahoga is a very nice framework and I love Cuyahoga development
    I shall admit that you may experience some problems while applying some advanced topics like Ajax to Cuyahoga. In this article
    I will show you a simple and structured way to add Ajax support to your Cuyahoga web site.

     

    Background

    I previously shared my module development experience with an article titled Developing Simple Issue Tracker Module here on CodeProject.
    In my issue tracker module I used AjaxToolkit ModalPopupExtender control. But this was an unstructured approach it was a kind of hack,
    I simply placed ScriptManager on my ASPX page and moved injection code of GeneralPage class to OnPreInit method from OnInit.
    This was the right choice and it saved the day, this module is still online and is working very well.

    Nowadays I am working on another web site BenimOdam.com (The web site is in Turkish and is not online yet due to domain name transfer issues).
    In BenimOdam.com we only use Forum and ContactUs built-in modules and much of the functionality is embedded in our own modules. These modules
    are mainly functioning as list and record editing modules. We used MultiView and View controls to provide tabbed browsing functionality and
    I guess much of you know that MultiView does a post back while switching between views and this post back may be annoying from the users point of view.
    In such uncomfortable situations UpdatePanel control included with Microsoft Ajax distribution(previously known as Atlas) provides a nice and easy to apply solution.
    You simply put your controls, MultiView in our case, inside an UpdatePanel and you are done. Your users will experience much more smooth
    navigation and probably they will be happier.

    Cuyahoga Internals

    Cuyahoga has some principles we must keep in mind before attempting to extend the framework for Ajax support. These are

    • Template user control with placeholders is used to identify different parts of your pages
    • Custom HttpHandler (PageHandler) is used to process requests and custom UrlWriter to rewrite raw urls.
    • Page structure(sections) and modules contained within the sections are resolved from the database
    • Injection is used to build the resulting pages.
    • Modules are designed as user Controls and you must inherit your module control from BaseModuleControl
    • You can use GeneralPage base class to build custom ASPX pages not releated to any Cuyahoga node that use the
      default site template.

    Ajax Support Preperation

    UpdatePanel included within Microsoft Ajax distirbution and AjaxToolkit controls all require a ScriptManager control placed
    as the first control in your ASPX page. But Cuyahoga does not handle your modules as seperate ASPX pages and injects
    your module inside the template you are using. As i mentioned above a template has placeholder controls that make the
    different parts of your pages. As a result it is not guaranteed that the ScripManager control you placed as the first
    control in your module will also be placed as the first control in the rendered page. Being the first control in the resulting
    page is a very tight constraint emposed by ASP.NET. Solution to this situation is placing a default ScriptManager control inside your
    template user controls as the first control. Here is a sample template user control code

     
    <%@ Control Language="c#" AutoEventWireup="false" Inherits="Cuyahoga.Web.UI.BaseTemplate" %> 
    <%@ Register TagPrefix="cc1" Namespace="Cuyahoga.ServerControls.Navigation" 
       Assembly="Cuyahoga.ServerControls.Navigation" %> 
    <%@ Register Assembly="System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
      Namespace="System.Web.UI" TagPrefix="asp" %> 
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 
    <html> 
    <head> 
        <title> <asp:literal id="PageTitle" runat="server"> </asp:literal> </title> 
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
        <asp:literal id="MetaTags" runat="server" /> 
        <asp:literal id="Stylesheets" runat="server" /> 
        <!--[if IE]> 
        <style type="text/css" media="screen"> 
            body { behavior: url(<%= Page.ResolveUrl("~/csshover.htc") %> ); /* call hover behaviour file */ } 
        </style> 
        <![endif]--> 
    </head> 
    <body> 
        <form id="t" method="post" runat="server"> 
            <asp:ScriptManager ID="DefaultScriptManager" runat="server"> </asp:ScriptManager> 
            <div id="container">             
                <div id="header">     
                    <div id="logo"> 
                     <img width="120px" height="100px"  alt="BenimOdam.com" 
                    src="http://www.mydomain.com/Templates/Bo/Images/bo_logosmaller.gif%22/> 
                    </div> 
                    <div> 
                        <span id="titletext"> Ev arkadaşı ve ev arayanların buluşma noktası.</span> 
                    </div>         
                    <div id="searcharea"> 
                        <asp:placeholder id="searchinput" runat="server"> </asp:placeholder> 
                    </div> 
                </div> 
                <div id="nav"> 
                    <cc1:menu id="mnuMain" runat="server" MenuCss = "~/Templates/Bo/Css/Menu.css"> </cc1:menu> 
                </div>     
    
    
                <!-- shadow divs --> 
                <div id="containerleft"> 
                <div id="containertopleft"> 
                <div id="containerright"> 
                <div id="containertopright"> 
                <!-- main --> 
                <div id="main"> 
                    <!--
                    <div id="globalmenu"> 
                        <asp:placeholder id="globalMenu" runat="server"> </asp:placeholder> 
                    </div> 
                    -->
    

    In the sample code above you can see that we added a ScriptManager named DefaultScriptManager as the first control
    of the form. That means DefaultScriptManager will be injected to all our Cuyahoga pages using this template. As a result we
    met the constraint saying that "Ajax controls need a ScriptManager and this script manager must be the first control in the ASPX page.".

    NOTE: Do not forget to register System.Web.Extensions.dll, else you will get an exception telling you that ScriptManager type can not be resolved.

    Adding Ajax Support To Your Modules

    In theory we do not have to add ScriptManager to our module code, since we added a default ScriptManager to our template which will
    automatically be injected to all of our Cuyahoga pages. But in practice we will probably want designer support while developing our modules
    and if you do not include ScriptManager in your module control the designer will complain and refuse to render the AJAX control
    which can make us feel uncomfortable (you can still design your module markup without designer support). When you add a ScriptManager
    to your module the resulting Cuyahoga page will contain more than one ScriptManager one from the template and one or many from your modules.
    Another constraint about ScriptManager says that "Only one ScriptManager can be used in a page" and we have to find a way to
    remove additional ScriptManager instances and leave only one ScriptManager in the resulting page. The solution here is straight forward
    we will remove ScriptManagers placed in our modules and leave only the DefaultScriptManager placed in our template control.

    LIMITATIONS If you want to use custom JavaScript code for AJAX handling in your module you have to register
    your scripts to your ScriptManager's (one included in your module code) Scripts collection. In this case you will have to rethink
    the solution I proposed. May be you will have to invent some interaction that places your custom scripts in the DefaultScriptManager
    before removing the ScriptManager from your module.

    As i mentioned above all of your Cuyahoga modules must be inherited from BaseModuleControl.But we have to find a way to remove
    ScriptManagers from our module code before they are injected to the template we are using. The solution is creating another base class ( AjaxBaseModuleControl)
    which supports ScripManager removal functionality. AjaxBaseModuleControl is inherited from BaseModuleControl and overrides AddedControl method.
    In the AddedControl function we try to catch the ScriptManager control after it is added to the Controls collection of our module and remove it from
    from the colllection so that multiple ScriptManagers problem is avoided.

    NOTE: We could prefer to modify PageEngine class so that we would inspect all controls and remove the ScriptManagers from the modules.
    But that would probably cause performance problems since we would have to loop with foreach on modules' Controls collection.May be some modules
    would not even use AJAX and that would be waste of time inspecting these modules for a ScriptManager control.
    ( Marker interfaces could be used to identify AJAX modules but still that would be wast of time to loop)

    Here is the AjaxBaseModuleControl code

     
    namespace Cuyahoga.Web.UI
    {
      public class AjaxBaseModuleControl:BaseModuleControl
      {
        protected override void AddedControl(Control control, int index)
        {
          if (control.GetType() == typeof(ScriptManager))
            this.Controls.RemoveAt(index);
          else
            base.AddedControl(control, index);
        }
      }
    }
    


    AjaxBaseModuleControl is simple and straightforward, we simply catch the ScriptManager after it is added to Controls collection
    and remove it from the collection, which enables us to avoid multiple ScriptManagers problem.

    IMPORTANT: Cuyahoga PageEngine class applies the template and injects your modules in the overriden OnInit function.
    I would recommend you to move OnInit code to the overriden OnPreInit function. That is not necessary for module level Ajax support
    but the reason will be more clear when I explain page level Ajax support.

    Adding Ajax Support To Your Nodeless Pages

    Modules are the primary mains of Cuyahoga development. But it is obvious that only modules may not meet all your requirements.
    For example you would list records with a module and deploy a seperate nodeless page for record editing. We call the record editing page
    nodeless because this page is not attached to any node in our site structure.For such cases Cuyahoga framework provides us a base
    class named GeneralPage. You inherit your nodeless page from GeneralPage and Cuyahoga automatically applies
    the default site template (CSS styles and structure of the page based on the default template) to your page. Actually
    your page code is injected not rendered.

    For nodeless page example please go to Pragma Issue Tracker and try to view an issue from the issue list. The page
    used to view a specific issue is a nodeless page and it is not included in the site structure we simply redirect to
    this page from our module and Cuyahoga injects the page code automatically.

    In order to add Ajax support to our nodeless pages we have to apply the same ideas.

    • Add default ScriptManager as the first control to the resulting page.
    • Automatically remove the ScriptManager, added during design time, before Cuyahoga injects our page's source


    First item was already applied by putting a default script manager to our template control. For the second item
    we create another base class named AjaxGeneralPage which is inherited from GeneralPage and
    override the AddedControl method to intercept and catch the ScriptManager included in our nodeless page.
    Here is the code:

     
    namespace Cuyahoga.Web.UI
    {
      public class AjaxGeneralPage:GeneralPage
      {
        protected override void AddedControl(Control control, int index)
        {
          if (control.GetType() == typeof(HtmlForm))
            TryToRemoveScriptManager(control as HtmlForm);
          else
            base.AddedControl(control, index);
        } 
        private void TryToRemoveScriptManager(HtmlForm frm)
        {
          int idx = -1;
          for (int i = 0; i <frm.Controls.Count; i++)
          {
            if (frm.Controls[i] is ScriptManager)
            {
              idx = i;
              break;
            }
          } 
          if (idx >= 0)
            frm.Controls.RemoveAt(idx);
        }
      }
    }  

    Please be warned that we do not catch the ScriptManager directly as that was the case in AjaxBaseModuleControl. We catch the
    HtmlForm control included within the page and search for ScriptManager in the form's Controls collection.

    IMPORTANT NOTE: Original version of GeneralPage class (base class of our AjaxGeneralPage) handles content
    loading in the overriden OnInit function (I think this was the only place in .NET 1.1 version to perform content loading).
    If you leave content loading code inside this function you will not be able to properly remove the ScriptManager control
    from your page. For details why this is not possible see ASP.NET Page Lifecylcle article on MSDN. To solve this problem
    we simply move the code in overriden OnInit function to OnPreInit override.That is the right place for content
    loading and dynamic control creation in .NET version 2.0.

    Our OnPreIniti function in GeneralPage class looks like this


     
    protected override void OnPreInit(EventArgs e)
    {
        // The GeneralPage loads it's own content. No need for the PageEngine to do that.
        base.ShouldLoadContent = false;
        
        //Init the PageEngine.
        //NOTE: We replaced base.OnInit with base.OnPreIniti
        base.OnPreInit(e);
        
        // Build page.
        ControlCollection col = this.Controls; 
    
        this._currentSite = base.RootNode.Site;
        if (this._currentSite.DefaultTemplate != null 
            && this._currentSite.DefaultPlaceholder != null 
            && this._currentSite.DefaultPlaceholder != String.Empty)
        {
            // Load the template
            this.TemplateControl = (BaseTemplate)this.LoadControl(UrlHelper.GetApplicationPath() 
                + this._currentSite.DefaultTemplate.Path); 
    
    
            // Register css
            string css = UrlHelper.GetApplicationPath() 
                + this._currentSite.DefaultTemplate.BasePath
                + "/Css/" + this._currentSite.DefaultTemplate.Css;
            RegisterStylesheet("maincss", css); 
    
    
            if (this._title != null)
            {
                this.TemplateControl.Title = this._title;
            } 
    
    
            // Add the pagecontrol on top of the control collection of the page
            this.TemplateControl.ID = "p";
            col.AddAt(0, this.TemplateControl); 
    
    
            // Get the Content placeholder
            this._contentPlaceHolder = this.TemplateControl.FindControl(this._currentSite.DefaultPlaceholder) as PlaceHolder;
            if (this._contentPlaceHolder != null)
            {
                // Iterate through the controls in the page to find the form control.
                foreach (Control control in col)
                {
                    if (control is HtmlForm)
                    {
                        // We've found the form control. Now move all child controls into the placeholder.
                        HtmlForm formControl = (HtmlForm)control;
                        while (formControl.Controls.Count > 0)
                        {    
                            this._contentPlaceHolder.Controls.Add(formControl.Controls[0]);                        
                        }
                    }
                } 
    
    
    
                // throw away all controls in the page, except the page control 
                while (col.Count > 1)
                {
                    col.Remove(col[1]);                
                }
            } 
    
    
            #region // Ali Ozgur (07-02-2008): Load sections that are related to the template
            foreach (DictionaryEntry sectionEntry in _currentSite.DefaultTemplate.Sections)
            {
                string placeholder = sectionEntry.Key.ToString();
                Section section = sectionEntry.Value as Section;
                if (section != null)
                {
                    BaseModuleControl moduleControl = CreateModuleControlForSection(section);
                    if (moduleControl != null)
                    {
                        ((PlaceHolder)this._templateControl.Containers[placeholder]).Controls.Add(moduleControl);
                    }
                }
            }
            #endregion 
        }
        else
        {
            // The default template and placeholders are not correctly configured.
            throw new Exception("Unable to display page because the default template is not configured.");
        }
    } 
    
    
    #region // Ali Ozgür 07-02-2008 : Load sections that are related to the template
    private BaseModuleControl CreateModuleControlForSection(Section section)
    {
        // Check view permissions before adding the section to the page.
        if (section.ViewAllowed(this.User.Identity))
        {
            // Create the module that is connected to the section.
            ModuleBase module = _moduleLoader.GetModuleFromSection(section); 
    
    
            if (module != null)
            {
                if (Context.Request.PathInfo.Length > 0 && section == this._activeSection)
                {
                    // Parse the PathInfo of the request because they can be the parameters 
                    // for the module that is connected to the active section.
                    module.ModulePathInfo = Context.Request.PathInfo;
                }
                return LoadModuleControl(module);
            }
        }
        return null;
    } 
    
    
    private BaseModuleControl LoadModuleControl(ModuleBase module)
    {
        BaseModuleControl ctrl = (BaseModuleControl)this.LoadControl(UrlHelper.GetApplicationPath() + module.CurrentViewControlPath);
        ctrl.Module = module;
        return ctrl;
    }
    #endregion 
    
    

    In the generalPage code snippet presented above you will notice regions of code that adds support for loading sections
    attached to the default site template. This code has nothing to do with AJAX support it was an improvement
    neede for BenimOdam.com

    Modifying the HttpHandler for AJAX support

    As I mentioned in Cuyahoga Internals section of the article, Cuyahoga framework registers a custom HttpHandler class named
    PageHandler to handle page requests. This handler is needed as the result of injection practice used
    in the framework. Cuyahoga does not actually renders physical pages or user controls. Page structure is
    retreived from the database (sections and modules within these sections)and modules are instantiated during runtime
    and the final page is constructed by Cuyahoga with injecting module code and page template to a resulting page.
    Since there is only one physical page called Default.aspx (actually there some more physical pages as Error.aspx and Install.aspx)
    all page request must be handled by a custom HttpHandler and resolved so that proper page with proper sections and modules
    can be constructed in runtime.

    PageHandler class implements IHttpHandler interface and IRequiresSessionState marker interface.
    PageHandler utilizes Cuyahoga'a custom UrlRewriter class which is used to rewrite requested URLs. UrlRewriter produces
    URLs that are meaningful for the framework and used for building the right reult page. But unfortunatelly http requests
    caused by AJAX calls can not be handled properly by PageHandler because Cuyahoga's UrlRewriter can not rewrite the right
    URL for AJAX calls which in turn results in reource not found exception thrown by the handler. To overcomde this problem
    we have to slightly modify PageHandler class's ProcessRequest function. Here is the code

     
    
    
    public void ProcessRequest(HttpContext context)
    {
        string rawUrl = context.Request.RawUrl;
        log.Info("Starting request for " + rawUrl);
        DateTime startTime = DateTime.Now; 
    
    
        string aspxPagePath = String.Empty;
        // Rewrite url
        UrlRewriter urlRewriter = new UrlRewriter(context);
        string rewrittenUrl = urlRewriter.RewriteUrl(rawUrl); 
    
    
        
        #region //Ali Ozgur: This is an ajax request, so we have to realign the rewritten url.
        if (context.Request["HTTP_X_MICROSOFTAJAX"] != null)
        {
            int idx = rewrittenUrl.ToLowerInvariant().IndexOf("/default.aspx");
            if (idx >= 0)
            {
                rewrittenUrl = rewrittenUrl.Substring(idx, rewrittenUrl.Length - idx);
            }
        }
        #endregion
        
        
        // Obtain the handler for the current page
        aspxPagePath = rewrittenUrl.Substring(0, rewrittenUrl.IndexOf(".aspx") + 5);
      
        IHttpHandler handler = PageParser.GetCompiledPageInstance(aspxPagePath, null, context); 
    
    
        // Process the page just like any other aspx page
        handler.ProcessRequest(context); 
    
    
        // Release loaded modules. These modules are added to the HttpContext.Items collection by the ModuleLoader.
        ReleaseModules(); 
    
    
        // Log duration
        TimeSpan duration = DateTime.Now - startTime;
        log.Info(String.Format("Request finshed. Total duration: {0} ms.", duration.Milliseconds));
    } 
    
    
    

    Installation

    We have to modify our Web.config file to enable Ajax support. If we do not add the following configuration information it is likely that
    we will get "Sys not defined" error when our module tries to execute Ajax related code.

    
    
    <system.web > 
     <httpHandlers > 
      <remove verb="*"  path="*.asmx" /> 
      <add verb="*"  path=" Error.aspx"  type=" System.Web.UI.PageHandlerFactory"  /> 
      <add verb="*"  path=" *.aspx"  type=" Cuyahoga.Web.HttpHandlers.PageHandler, Cuyahoga.Web"  /> 
      <add verb="*"  path=" *.asmx"  validate=" false"  
          type=" System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> 
      <add verb="*"  path=" *_AppService.axd"  validate=" false"  
         type=" System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> 
      <add verb=" GET,HEAD"  path=" ScriptResource.axd"  type=" System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"  validate=" false" /> 
     </httpHandlers> 
     <httpModules> 
      <add type=" Cuyahoga.Web.HttpModules.AuthenticationModule, Cuyahoga.Web"  
        name=" AuthenticationModule"  /> 
      <add type=" Cuyahoga.Web.HttpModules.CoreRepositoryModule, Cuyahoga.Web"  
        name=" CoreRepositoryModule"  /> 
      <add name=" NHibernateSessionWebModule"  
       type=" Castle.Facilities.NHibernateIntegration.Components.SessionWebModule, Castle.Facilities.NHibernateIntegration"  /> 
                
      <!--Ajax toolkit support--> 
      <add name=" ScriptModule"  
       type=" System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> 
    
    
    </httpModules> 
    </system.web>  
    
    
    

    History

    18 February 2008: Initiali version published


    Posted in: .NET Development , CodeProject , Cuyahoga , NHibernate  Tags:

    Be the first to rate this post

    • Currently 0/5 Stars.
    • 1
    • 2
    • 3
    • 4
    • 5
    aliozgur posted on February 6, 2008 16:28

    Introduction

    Audit logging is an important issue while building enterprise systems. Simplest form of audit logging is recording
    who and when created/updated an object, or a record in the database respectively. We perform four basic operations
    on a domain object. These are

    Load
    Update
    Save
    Delete


    Data access layer is a good place to perform automated audit logging
    whenever one of the operations above is executed. NHibernate provides us with
    IInterceptor interface plus ILifecylce interface. You can read this article for more
    information about Nhibernate entity lifecyle management.

    In this article we will try to perform simple logging that meets the
    following minimal requirements


    1- We will log who performed insert/update and when this operation was performed
    2- Log data will be written to the same database and table as our domain object

    Class Model

    We have three interfaces directly related to audit logging and IInterceptor implementation, one interface
    (IVersionedEntity) which is in the model just for conceptual completiness and is used
    to indicate that we want to utilize NHibernate managed versioning, one base DomainObject
    from which all our domain objects inherit and finally three implementation classes that
    implement IInsertLoggable, IModifyLoggable and IUpdateLoggable interfaces.

     
    
    
    public abstract class InsertLoggableDomainObject : 
                DomainObject,IVersionedEntity, IInsertLoggable
    {
      #region IVersionedEntity Members 
    
    
      private long _version = 0;
      public long Version
      {
        get { return _version; }
      } 
    
    
      #endregion 
    
    
      #region IInsertLogable Members 
    
    
      private DateTime? _sysCreatedOn = null;
      public DateTime? SysCreatedOn
      {
        get { return _sysCreatedOn; }
        set { _sysCreatedOn = value; }
      } 
    
    
      private int? _sysCreatedBy = null;
      public int? SysCreatedBy
      {
        get { return _sysCreatedBy; }
        set { _sysCreatedBy = value; }
      }
      #endregion
    } 
    
    
    public abstract class UpdateLoggableDomainObject : 
                DomainObject, IVersionedEntity, IUpdateLoggable
    {
      #region IVersionedEntity Members 
    
    
      private long _version = 0;
      public long Version
      {
        get { return _version; }
      } 
    
    
      #endregion 
    
    
      #region IUpdateLogable Members 
    
    
      private DateTime? _sysLastUpdatedOn = null;
      public DateTime? SysLastUpdatedOn
      {
        get { return _sysLastUpdatedOn; }
        set { _sysLastUpdatedOn = value; }
      } 
    
    
      private int? _sysLastUpdatedBy = null;
      public int? SysLastUpdatedBy
      {
        get { return _sysLastUpdatedBy; }
        set { _sysLastUpdatedBy = value; }
      }
      #endregion
    } 
    
    
    public abstract class ModifyLoggableDomainObject : 
                DomainObject, IVersionedEntity, IModifyLogable
    {
      #region IVersionedEntity Members 
    
    
      private long _version = 0;
      public long Version
      {
        get { return _version; }
      } 
    
    
      #endregion 
    
    
      #region IModifyLogable Members 
    
    
      private DateTime? _sysCreatedOn = null;
      public DateTime? SysCreatedOn
      {
        get { return _sysCreatedOn; }
        set { _sysCreatedOn = value; }
      } 
    
    
      private int? _sysCreatedBy = null;
      public int? SysCreatedBy
      {
        get { return _sysCreatedBy; }
        set { _sysCreatedBy = value; }
      } 
    
    
      private DateTime? _sysLastUpdatedOn = null;
      public DateTime? SysLastUpdatedOn
      {
        get { return _sysLastUpdatedOn; }
        set { _sysLastUpdatedOn = value; }
      } 
    
    
      private int? _sysLastUpdatedBy = null;
      public int? SysLastUpdatedBy
      {
        get { return _sysLastUpdatedBy; }
        set { _sysLastUpdatedBy = value; }
      }
      #endregion
    } 
    
    
    
     

    If we want our domain objects to be audit logged by our NHibernate IInterceptor instead of solely inheriting from
    DomainObject we may inherit our domain objects from one of the loggable domain object implementations
    (InsertLoggableDomainObject, UpdateLoggableDomainObject, ModifyLoggableDomainObject)

    NHibernate Setup

    Inheriting our domain objects from one of the base loggable classes does not provide us
    full logging support.

    NHibernate Mappings

    As any other NHibernate utilization needs some mapping work this sample also needs some NHibernate mapping.
    All our loggable domain objects must include the following mapping information

     
    
    
    <?xml version='1.0' encoding='utf-8'?>
    <hibernate-mapping
        xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
        xmlns:xsd='http://www.w3.org/2001/XMLSchema' 
                    xmlns='urn:nhibernate-mapping-2.0'>
      <class
          name='Fully Qualified Class name comes here'
          table='Table name'>
        <id name='Identity field name'
            column='Identity column to which identity field maps'
            unsaved-value='0'>
          <generator class='identity'/>
        </id>
        <!--Managed versioning support. Defined in IVersionedEntity interface -->
        <version name='_version' column='Version' access='field' 
                        unsaved-value='0' type='Int64'/> 
    
    
        <!--Log field mappings -->
        <property name='SysCreatedOn' column='SysCreatedOn' 
                type='NHibernate.Nullables2.NullableDateTimeType, 
                NHibernate.Nullables2'/>
        <property name='SysCreatedBy' column='SysCreatedBy' 
                type='NHibernate.Nullables2.NullableInt32Type, 
                NHibernate.Nullables2'/>
        <property name='SysLastUpdatedOn' column='SysLastUpdatedOn' 
                type='NHibernate.Nullables2.NullableDateTimeType, 
                NHibernate.Nullables2'/>
        <property name='SysLastUpdatedBy' column='SysLastUpdatedBy' 
                type='NHibernate.Nullables2.NullableInt32Type, 
                NHibernate.Nullables2'/>
        <!-- Some other field, relation, subclass and other kind of mappings 
                            from this point on-->
      </class>
    </hibernate-mapping> 
    
    
    
     

    We use NHibernate.Nullables2 (Found in NHibernate Contributions) for type definitions of our log data fields.
    We can simply ommit this reference and usage of nullables by simply changing interface field definitions
    and their implementations from int? to int and DateTime? to DateTime. But be warned that SysCreatedBy
    and SysLastUpdatedBy columns are defined as foreign keys to another table in the database, say Person.
    Thus making these fields non-nullable may cause constraint violations on the db side.

    NHibernate Session Initialization

    This step is simple.Either you directly manage NHibernate sessions from your own code
    or you use simple but yet very powerfull NHSessionManager singleton
    you have to use ISession ISessionFactory.OpenSession(IInterceptor interceptor)
    overload from NHibernate assembly to open a session. IInterceptor interceptor parameter here
    is an instance of our IInterceptor implementation.

    IInterceptor Implementation

    You can thing of an interceptor as a hook utility to data access layer. IInterceptor implementation
    will look like this

     
    
    
    public class MyAuditLogger:IInterceptor
    {
      #region IInterceptor Members 
    
    
      public int[] FindDirty(object entity, object id, 
                object[] currentState, object[] previousState, 
                string[] propertyNames, 
                global::NHibernate.Type.IType[] types)
      {
          return null;
      } 
    
    
      public object Instantiate(Type type, object id)
      {
          return null;                
      } 
    
    
      public object IsUnsaved(object entity)
      {
          return null;
      } 
    
    
      public void OnDelete(object entity, object id, object[] state, 
        string[] propertyNames, global::NHibernate.Type.IType[] types)
      {
      } 
    
    
      public bool OnFlushDirty(object entity, object id, object[] currentState, 
            object[] previousState, string[] propertyNames, 
            global::NHibernate.Type.IType[] types)
      {
          if (entity is IUpdateLogable)
          {
              SetUpdateLoggableValues(currentState, propertyNames);
              return true;
          }
          else if (entity is IModifyLogable)
          {
              SetModifyLoggableValues_OnUpdate(currentState, propertyNames);
              return true;
          }
          else
          {
              return true;
          }
      } 
    
    
      public bool OnLoad(object entity, object id, object[] state, 
        string[] propertyNames, global::NHibernate.Type.IType[] types)
      {
          return true;
      } 
    
    
      public bool OnSave(object entity, object id, object[] state, 
        string[] propertyNames, global::NHibernate.Type.IType[] types)
      {
          if (entity is IInsertLogable)
          {
              SetInsertLoggableValues(state, propertyNames);
              return true;
          }
          else if (entity is IModifyLogable)
          {
              SetModifyLoggableValues_OnInsert(state, propertyNames);
              return true;
          }
          else
          {
              return true;
          }
      } 
    
    
      public void PostFlush(System.Collections.ICollection entities)
      { 
    
    
      } 
    
    
      public void PreFlush(System.Collections.ICollection entities)
      { 
    
    
      } 
    
    
      private Hashtable GetInsertLoggablePropertyIndexes(string[] Properties)
      {
          Hashtable result = new Hashtable();
          int propCounter = 0;
          for (int i = 0; i < Properties.Length; i++)
          {
              if (Properties[i].ToLower() == "syscreatedby")
              {
                  propCounter++;
                  result.Add("syscreatedby", i);
              }
              else if (Properties[i].ToLower() == "syscreatedon")
              {
                  propCounter++;
                  result.Add("syscreatedon", i);
              } 
    
    
              if (propCounter == 2)
              {
                  break;
              }
          }
          return result;  
      } 
    
    
      private Hashtable GetUpdateLoggablePropertyIndexes(string[] Properties)
      {
          Hashtable result = new Hashtable();
          int propCounter = 0;
          for (int i = 0; i < Properties.Length ; i++)
          {
              if (Properties[i].ToLower() == "syslastupdatedby")
              {
                  propCounter++;
                  result.Add("syslastupdatedby", i);
              }
              else if (Properties[i].ToLower() == "syslastupdatedon")
              {
                  propCounter++;
                  result.Add("syslastupdatedon", i);
              } 
    
    
              if (propCounter == 2)
              {
                  break;
              }
          }
          return result;
      } 
    
    
      private Hashtable GetModifyLoggablePropertyIndexes(string[] Properties)
      {
          Hashtable result = new Hashtable();
          int propCounter = 0;
          for (int i = 0; i < Properties.Length; i++)
          {
              if (Properties[i].ToLower() == "syscreatedby")
              {
                  propCounter++;
                  result.Add("syscreatedby", i);
              }
              else if (Properties[i].ToLower() == "syscreatedon")
              {
                  propCounter++;
                  result.Add("syscreatedon", i);
              }
              else if (Properties[i].ToLower() == "syslastupdatedby")
              {
                  propCounter++;
                  result.Add("syslastupdatedby", i);
              }
              else if (Properties[i].ToLower() == "syslastupdatedon")
              {
                  propCounter++;
                  result.Add("syslastupdatedon", i);
              } 
    
    
              if (propCounter == 4)
              {
                  break;
              }
          }
          return result;
      } 
    
    
      private void SetInsertLoggableValues(object[] state, string[] Properties)
      {
          Hashtable indexes = GetInsertLoggablePropertyIndexes(Properties);
          if (indexes.Count != 2)
          {
              throw new Exception("Can't log IInsertLoggable entity. 
    
                            Indexes not found!");
    
          } 
    
    
          int index = -1;
          
          if(indexes["syscreatedby"] == null)
          {
              throw new Exception("Can't log IInsertLoggable entity. 
    
                Index value for SysCreatedBy does not exist!");            
    
          }
          index = (int)indexes["syscreatedby"];
          state[index] = ContextManager.Instance.ActivePersonID; 
    
    
          if (indexes["syscreatedon"] == null)
          {
              throw new Exception("Can't log IInsertLoggable entity. 
    
                Index value for SysCreatedOn does not exist!");
    
          }
          index = (int)indexes["syscreatedon"];
          state[index] = DateTime.Now; 
    
    
      } 
    
    
      private void SetUpdateLoggableValues(object[] state, string[] Properties)
      {
          Hashtable indexes = GetUpdateLoggablePropertyIndexes(Properties);
          if (indexes.Count != 2)
          {
              throw new Exception("Can't log IUpdateLoggable entity. 
    
                                Indexes not found!");
    
          } 
    
    
          int index = -1; 
    
    
          if (indexes["syslastupdatedby"] == null)
          {
              throw new Exception("Can't log IUpdateLoggable entity. 
    
                Index value for SysLastUpdatedBy does not exist!");
    
          }
          index = (int)indexes["syslastupdatedby"];
          state[index] = ContextManager.Instance.ActivePersonID; 
    
    
          if (indexes["syslastupdatedon"] == null)
          {
              throw new Exception("Can't log IUpdateLoggable entity. 
    
                Index value for SysLastUpdatedOn does not exist!");
    
          }
          index = (int)indexes["syslastupdatedon"];
          state[index] = DateTime.Now; 
    
    
      } 
    
    
      private void SetModifyLoggableValues_OnInsert(object[] state, 
                                string[] Properties)
      {
          Hashtable indexes = GetModifyLoggablePropertyIndexes(Properties);
          if (indexes.Count != 4)
          {
              throw new Exception("Can't log IInsertLoggable entity. 
    
                                Indexes not found!");
    
          } 
    
    
          int index = -1; 
    
    
          if (indexes["syscreatedby"] == null)
          {
              throw new Exception("Can't log IInsertLoggable entity. 
    
                Index value for SysCreatedBy does not exist!");
    
          }
          index = (int)indexes["syscreatedby"];
          state[index] = ContextManager.Instance.ActivePersonID; 
    
    
          if (indexes["syscreatedon"] == null)
          {
              throw new Exception("Can't log IInsertLoggable entity. 
    
                Index value for SysCreatedOn does not exist!");
    
          }
          index = (int)indexes["syscreatedon"];
          state[index] = DateTime.Now; 
    
    
      } 
    
    
      private void SetModifyLoggableValues_OnUpdate(object[] state, 
                                string[] Properties)
      {
          Hashtable indexes = GetModifyLoggablePropertyIndexes(Properties);
          if (indexes.Count != 4)
          {
              throw new Exception("Can't log IModifyLogable entity. 
    
                                Indexes not found!");
    
          } 
    
    
          int index = -1; 
    
    
          if (indexes["syslastupdatedby"] == null)
          {
              throw new Exception("Can't log IModifyLogable entity. 
    
                Index value for SysLastUpdatedBy does not exist!");
    
          }
          index = (int)indexes["syslastupdatedby"];
          state[index] = ContextManager.Instance.ActivePersonID; 
    
    
          if (indexes["syslastupdatedon"] == null)
          {
              throw new Exception("Can't log IModifyLogable entity. 
    
                Index value for SysLastUpdatedOn does not exist!");
    
          }
          index = (int)indexes["syslastupdatedon"];
          state[index] = DateTime.Now;
      }
      
      #endregion
    } 
    
    
    
     

    Actually we only implemented OnFlushDirty and OnSave methods of the IInterceptor
    and left other interface methods to return null or empty. Following methods are utility methods
    that help us to find out domain object type and fill the appropriate log data

    Property validation methods: These methods simply loop through domain object properties
    ,already supported by NHibernate, to determine if log fields (SysCreatedBy, SysLastUpdatedBy,SysCreatedOn,SysLastUpdatedOn)
    has NHibernate mapping definitions. If NHibernate mapping definitions does not exist for these fields
    exception is thrown by the interceptor.
    - GetInsertLoggablePropertyIndexes
    - GetUpdateLoggablePropertyIndexes
    - GetModifyLoggablePropertyIndexes

    Log data setters: These methods simply set log data (SysCreatedBy, SysLastUpdatedBy,SysCreatedOn,SysLastUpdatedOn)
    based on the loıggable interface implemented by the domain object.
    -SetInsertLoggableValues
    -SetUpdateLoggableValues
    -SetModifyLoggableValues_OnInsert
    -SetModifyLoggableValues_OnUpdate

    Remarks and Future Work Suggestions

    The sample implementation was one of the my first experiences with NHibernate.So it may not sound
    very efficient to you. But i beleive this sample will be a good point to start

    I've read about some difficulties and tips about performing interceptor operations and logging
    using the NHibernate session interceptor is already attached to

    Further work can focus on logging to different database by defining loggable domain object
    implementations as reusable domain objects with their own mappings


    Posted in: .NET Development , CodeProject , NHibernate  Tags:

    Be the first to rate this post

    • Currently 0/5 Stars.
    • 1
    • 2
    • 3
    • 4
    • 5