Wednesday, May 2, 2012

Extracting MSDS Finished Goods from MAPICS/38 using PL/1

Okay, this has to take the cake for a rare combination…a PL/1 program that walks down an assembly chain for a finished good product and explodes out the complete list of raw materials that make up the product. I had to build a “shadow” bill of materials for each finished good item that consisted of the raw materials so I could then simulate chemical reactions and determine the remaining ingredients that where left. If these ingredients were then above certain thresholds they have to be declared on a Material Safety Data Sheet. Wonder how it might look in C# in an OO style using today’s patterns?

/*   MSD710P - Load a Product Structure into MSDS from PDM   */

 MSD710P: PROCEDURE (BR_ITEM_IN) RETURNS(CHAR(2));

 /********************************************************************/
 /*     NAME: MSD710P                                                */
 /*  PURPOSE: Load the Mapics/38 Product Structure into MSDS System  */
 /*  WRITTEN: 01/30/90                                               */
 /*   AUTHOR: John Watson                                            */
 /*   SYSTEM: IBM System/38                                          */
 /* LANGUAGE: PL/I                                                   */
 /* SECURITY: None                                                   */
 /*    DESCR: This pgm extracts the product structure of an item     */
 /*           from the Mapics/38 Product Structure file (PSTRUC)     */
 /*           and builds the MSDS Finished Good/Component file       */
 /*           (COMPNT).  Only final raw materials are output to the  */
 /*           MSDS system.  That is, phantoms and sub-assemblies are */
 /*           exploded down to their raw materials as well.  The raw */
 /*           material quantities are converted to pounds or gallons */
 /*           in a batch size of 100 (100 lbs / 100 gals) for the    */
 /*           100 Unit Formula used by the MSDS system.              */
 /*              NOTE:  This is designed as a function to            */
 /*                   FACILITATe returning to the caller             */
 /*                     with return codes.                           */
 /*                                                                  */
 /*   RETURN: 00 = Successful completion                             */
 /*           1x = Problem w/ finished good item                     */
 /*            0 =   Item A record not found                         */
 /*            1 =   Deleted Item A                                  */
 /*            2 =   Assembly chain not found                        */
 /*           2x = Problem w/ a component                            */
 /*            0 =   PSTRUC not found, assy chain broken/bad         */
 /*            1 =   component Item A record not found               */
 /*            2 =   RRNs don't match                                */
 /*            3 =   component Item A deleted                        */
 /*            4 =   compare portions don't match                    */
 /*            5 =   bad low level code (exceeds 99)                 */
 /*            6 =   MSFILM record not found - cannot convert qty    */
 /*                                                                  */
 /********************************************************************/

 /* DECLARE FILES */
 DCL
  ITEMAKEY FILE RECORD INPUT DIRECT KEYED INTERNAL
             ENV(BUFSIZE(256) INDEXED KEYDISP(1) KEYLENGTH(16)),
  ITEMARRN FILE RECORD INPUT DIRECT ENV(BUFSIZE(256)) INTERNAL,
  ITEMAC   FILE RECORD INPUT DIRECT KEYED ENV(DESCRIBED INDEXED),
  PSTRUC   FILE RECORD INPUT DIRECT ENV(BUFSIZE(96)) INTERNAL,
  MSFILMPF FILE RECORD INPUT DIRECT KEYED ENV(DESCRIBED INDEXED),
  MSBSRMPF FILE RECORD SEQUENTIAL KEYED ENV(DESCRIBED INDEXED);

 /* DECLARE RECORDS FOR INPUT FROM FILES */

 DCL 1 COMPNT,
           %INCLUDE MSBSRMPF(COMPNT,RECORD);
 DCL 1 CFMT,
           %INCLUDE ITEMAC(CFMT,RECORD);
 DCL 1 RMFILM,
           %INCLUDE MSFILMPF(RMFILM,RECORD);
   /**************************************************/
   /* ITEMAS is internally described because we need */
   /* std batch qty of parent from pos. 244-249.     */
   /**************************************************/
 DCL 1 ITEMA_P,
       2  PR_ACREC  CHAR(1),    /* active record code                */
       2  PR_ITNBR  CHAR(15),   /* item number                       */
       2  PR_FILL1  CHAR(1),    /* filler 1                          */
       2  PR_IIREC  DEC(7,0),   /* RRN for this item                 */
       2  PR_LOLEV  PIC '9R',   /* low level code                    */
       2  PR_IIFAC  DEC(7,0),   /* RRN of first assembly component   */
       2  PR_FILL2  CHAR(105),  /* filler 2                          */
       2  PR_UNMSR  CHAR(2),    /* unit of measure                   */
       2  PR_ITTYP  CHAR(1),    /* item type                         */
       2  PR_FILL3  CHAR(108),  /* filler 3                          */
       2  PR_SBQTY  DEC(11,3),  /* standard batch quantity           */
       2  PR_FILL4  CHAR(7);    /* filler 4                          */
 DCL 1 ITEMA_C,
       2  PC_ACREC  CHAR(1),    /* active record code                */
       2  PC_ITNBR  CHAR(15),   /* item number                       */
       2  PC_FILL1  CHAR(1),    /* filler 1                          */
       2  PC_IIREC  DEC(7,0),   /* RRN for this item                 */
       2  PC_LOLEV  PIC '9R',   /* low level code                    */
       2  PC_IIFAC  DEC(7,0),   /* RRN of first assembly component   */
       2  PC_FILL2  CHAR(105),  /* filler 2                          */
       2  PC_UNMSR  CHAR(2),    /* unit of measure                   */
       2  PC_ITTYP  CHAR(1),    /* item type                         */
       2  PC_FILL3  CHAR(108),  /* filler 3                          */
       2  PC_SBQTY  DEC(11,3),  /* standard batch quantity           */
       2  PC_FILL4  CHAR(7);    /* filler 4                          */
 DCL 1 PSTRUCT,
       2  PPCIM     DEC(7,0),   /* component item A RRN              */
       2  PPPIM     DEC(7,0),   /* parent item "A" RRN               */
       2  PPNAC     DEC(7,0),   /* next assembly component RRN       */
       2  PPFIL1    CHAR(8),    /* filler 1                          */
       2  CCMPI     CHAR(1),    /* compare portion of item number    */
       2  PCOMP     CHAR(1),    /* compare portion of parent item    */
       2  PPFIL2    CHAR(16),   /* filler 2                          */
       2  EDATM     DEC(6,0),   /* effective from date               */
       2  EDATO     DEC(6,0),   /* effective to date                 */
       2  PPFIL3    CHAR(44),   /* filler 3                          */
       2  QTYPR     DEC(11,3);  /* quantity per unit                 */

 /* DECLARE VARIABLES */
 DCL BATCH_SZ         DEC(7,3),  /* batch size */
     CMP(100)         CHAR(1),   /* compare characters */
     CMP_CHAR         CHAR(1),   /* compare character  */
     CNTRL_ID         CHAR(3),   /* used to redefine PPCIM */
     BR_ITEM_IN       CHAR(6),
     LVL              DEC(3,0),  /* array subscript */
     STD_BAT_QTY      DEC(11,3),
     RRN(100)         DEC(7,0),  /* RRNs to chase down assy chains   */
     REC_NO           DEC(7,0),  /* RRN workfield */
     SBQ(100)         DEC(11,3), /* Std batch qty for a given level  */
     WRK_QTY          FLOAT(7);

 /* SYSTEM DATE */
 DCL CHR_DATE        CHAR(6),
     TST_DATE        DEC(6,0);    /* YYMMDD */

 DCL 1 PROGRAM_FLAGS STATIC,
       2 NO                 BIT(1) ALIGNED INIT('0'B),
       2 YES                BIT(1) ALIGNED INIT('1'B),
       2 VALID_ITEM_A       BIT(1) ALIGNED,
       2 VALID_ITEM_C       BIT(1) ALIGNED,
       2 VALID_PSTRUC       BIT(1) ALIGNED,
       2 VALID_RMFILM       BIT(1) ALIGNED,
       2 MORE_COMPNT_RECS   BIT(1) ALIGNED;

 /* OPEN FILES */
 OPEN FILE(ITEMAKEY);
 OPEN FILE(ITEMARRN);
 OPEN FILE(ITEMAC);
 OPEN FILE(MSFILMPF);
 OPEN FILE(PSTRUC);

 /* ERROR HANDLING */
 ON KEY(ITEMAKEY) VALID_ITEM_A=NO;
 ON KEY(ITEMARRN) VALID_ITEM_A=NO;
 ON KEY(ITEMAC) VALID_ITEM_C=NO;
 ON KEY(PSTRUC) VALID_PSTRUC=NO;
 ON KEY(MSFILMPF) VALID_RMFILM=NO;
 ON KEY(MSBSRMPF) MORE_COMPNT_RECS=NO;
 ON ENDFILE(MSBSRMPF) MORE_COMPNT_RECS=NO;

 %PAGE;
 /*    M A I N L I N E   */

 CHR_DATE=DATE();
 TST_DATE=DECIMAL(CHR_DATE,6,0);

 BATCH_SZ=100;
 LVL=1;
 RRN=0;
 SBQ=0;
 CMP=' ';

    /* Get parent Item A */

 VALID_ITEM_A=YES;
 READ FILE(ITEMAKEY) INTO(ITEMA_P) KEY(BR_ITEM_IN);
 SELECT;
   WHEN (VALID_ITEM_A=NO) RETURN('10');       /* record not found */
   WHEN (PR_ACREC='D') RETURN('11');          /* deleted record */
   WHEN (PR_IIFAC=9999999) RETURN('12');      /* no assembly chain */
   OTHERWISE;
   END;
 REC_NO=PR_IIFAC;
 STD_BAT_QTY=PR_SBQTY;
 CMP_CHAR=SUBSTR(PR_ITNBR,1,1);

        /* Clear out old COMPNT records */

 OPEN FILE(MSBSRMPF) UPDATE;
 MORE_COMPNT_RECS=YES;
 READ FILE(MSBSRMPF) INTO (COMPNT) KEY(BR_ITEM_IN);
 DO WHILE (MORE_COMPNT_RECS);
   DELETE FILE(MSBSRMPF);
   READ FILE(MSBSRMPF) INTO (COMPNT);
   IF BR_PARENTª=BR_ITEM_IN THEN MORE_COMPNT_RECS=NO;
   END;
 CLOSE FILE(MSBSRMPF);
 OPEN FILE(MSBSRMPF) OUTPUT;

   /* begin down an assembly chain */

 TOP:;
 RRN(LVL)=REC_NO;               /* point to first component */
 SBQ(LVL)=BATCH_SZ;             /* set batch size */
 CMP(LVL)=CMP_CHAR;             /* set compare values */

   /* start of loop for next component */

 CHAIN: DO UNTIL(REC_NO=9999999);

       /* get next component */

   VALID_PSTRUC=YES;
   READ FILE(PSTRUC) INTO(PSTRUCT) KEY(REC_NO);
   IF VALID_PSTRUC=NO THEN RETURN('20');
       /* check for control record */
   CNTRL_ID=CHAR(PPCIM,3);
   IF CNTRL_ID='.PS' THEN RETURN('20');
       /* check dates */
   IF (EDATM>=TST_DATE) ³ (EDATOª=0 & EDATO<=TST_DATE) THEN DO;
      REC_NO=PPNAC;  /* outside dates - get next component */
      ITERATE CHAIN;
      END;

      /* get component Item A */

   VALID_ITEM_A=YES;
   READ FILE(ITEMARRN) INTO(ITEMA_C) KEY(PPCIM);
   SELECT;
     WHEN(VALID_ITEM_A=NO) RETURN('21');         /* no comp. Item A */
     WHEN(PC_IIRECª=PPCIM) RETURN('22');         /* RRNs don't match */
     WHEN(PC_ACREC='D') RETURN('23');            /* Item A deleted */
     WHEN(SUBSTR(PC_ITNBR,1,1)ª=CCMPI) RETURN('24'); /* compare char */
     WHEN(PC_LOLEV>99) RETURN('25');              /* bad low level */
     OTHERWISE;
     END;

      /* Check if this component is an intermediate. */
      /* If so, push current parent's values into    */
      /* arrays, set up work fields with new parent  */
      /* information and goto top to start down      */
      /* the chain of the new parent.                */

   IF (PC_ITTYP='1') ³ (PC_IIFACª=9999999) THEN DO;
        /* save current parent values */
     RRN(LVL)=PPNAC;
     SBQ(LVL)=STD_BAT_QTY;
     CMP(LVL)=CMP_CHAR;
        /* set up work fields with new parent values */
     REC_NO=PPNAC;
     STD_BAT_QTY=PR_SBQTY;
     CMP_CHAR=SUBSTR(PC_ITNBR,1,1);
        /* increase current level */
     LVL=LVL+1;
     GOTO TOP;
     END;

      /* get component Item C */

   READ FILE(ITEMAC) INTO (CFMT) KEY(PC_ITNBR);

      /* extend component qty & convert to lbs */

   WRK_QTY=BATCH_SZ/STD_BAT_QTY*QTYPR;
   SELECT;
      WHEN(PC_UNMSR='LB');
      WHEN(PC_UNMSR='GA') WRK_QTY=WRK_QTY*MC#GAL;
      WHEN(PC_UNMSR='GM') WRK_QTY=WRK_QTY/453.59;
      WHEN(PC_UNMSR='FL') WRK_QTY=(WRK_QTY*MC#GAL)/128;
      WHEN(PC_UNMSR='OZ') WRK_QTY=WRK_QTY/16;
      WHEN(PC_UNMSR='LY') DO;
        READ FILE(MSFILMPF) INTO(RMFILM) KEY(PC_ITNBR);
        IF VALID_RMFILM THEN DO;
          WRK_QTY=WRK_QTY*RF_WIDTH*RF_THICKNESS*.155844*MC#GAL;
          END;
        ELSE RETURN('26');   /* no MSFILMPF record */
        END;
      OTHERWISE;
      END;

      /* write out COMPNT */

   BR_COMP_QTY=WRK_QTY;
   BR_PARENT=BR_ITEM_IN;
   BR_COMPONENT=SUBSTR(PC_ITNBR,1,6);
   WRITE FILE(MSBSRMPF) FROM (COMPNT) KEYFROM(*);

   REC_NO=PPNAC;
 END;  /* DO group */

    /* At this point, we have reached the end of an  */
    /* assembly chain (PPNAC=9999999).  Check to see */
    /* if we encountered any intermediates along the */
    /* way.  If we did, pull back up one level and   */
    /* continue down the previous parent's chain.    */

 DO WHILE(LVL>1);
   LVL=LVL-1;
   IF RRN(LVL)ª=9999999 THEN DO;
     REC_NO=RRN(LVL);
     STD_BAT_QTY=SBQ(LVL);
     CMP_CHAR=CMP(LVL);
     GOTO CHAIN;
     END;
   END;

 RETURN('00');   /* DONE! */
 END MSD710P;

Code Generation circa 1988 using RPG II 1/2

While cleaning out my shelves I came across this old code sample disk. The company is long gone and the language is well…small. RPG II’s roots went back to punch-card days:
  • Column position on each line was important
  • Column 6 contains a one letter “record type”, e.g. H = header, C = calculation
  • The OPCODE in the middle of the calc spec was max 6 chars long, e.g. EXSR = execute subroutine.
If I remember, the “1/2” was a commercial 3rd party tool/enhancement that added stuff like “IF” and “DO” instead of having to use the left-hand indicators (e.g. N90) to condition execution of the line.
0001 H       024                B        1                  SAL010
0002 H*===================================================================
0003 H*@ PROPERTY OF XXXXXXXXXXXX, KEENE, NEW HAMPSHIRE 1988
0004 H*T Generate #GSORT OCL to retrieve customer/item sales history
0005 H*M  144 07Dec88 Written *** Uses RPG II 1/2 ***
0005 H*D Accepts up to 18 selection criterion and generates the necessary
0005 H*D OCL code to run #GSORT to extract/sort the customer/item sales
0005 H*D history file. The calling report proc is stored in LDA to
0005 H*D determine various report-specific sort specs such as the header
0005 H*D and field specs.  To add another procedure the following must
0005 H*D be done:  Add proc name to PROC array, add the new sort header
0005 H*D spec to HDR, add the record type spec(s) to REC if necessary,
0006 H*S EDIT$    Edits the selection criteria
0006 H*S INIT$    Program Initialization
0009 H*I 40-57    Screen error indicators.
0014 H*===============================================================
0015 FWORKSTN CD  E               WORKSTN
0019 FPROC    O     120 120           DISK
0021 E* workstation arrays
0022 E              $FN         18  2 0         field number
0022 E              $OP         18  2         operation code
0022 E              $CR         18 15         selection criteria
0021 E* program work arrays
0022 E              OPNS      6   6  2         operation codes
0022 E              PROC      8   8  6         procedure names
0022 E              TABPRC  8   8  6   TABNUM  2 0 proc names & # of #GSORT field specs
0021 E* sort specs (OCL code)
0022 E              BEG      1   5 80         common 'beginning' OCL statements
0022 E              HDR      1   8 80         header specs
0022 E              REC      1  11 80         record selection specs
0037 I         UDS
0038 I                          1   8 @USER
0039 I                          9  10 @WS
     I                        505 512 @PROC       ?WS? + ?TIME?
0040 C*==============================================HILOEQ===========
0042 C     #@INIT    IFNE 1
0043 C               EXSR INIT$
0044 C               END
     C*
     C     T$1000    TAG
     C               EXFMTSCRN100
     C  KG           SETON             U7
     C  KG           GOTO T$9999
     C* call edit routine to check for errors
     C               EXSR EDIT$
     C* if any error occured, redisplay screen
     C     #@ERR     IFEQ 1
     C               GOTO T$1000
     C               END
     C* if Cmd/1 wasn't pressed loop back and redisplay
     C NKA           GOTO T$1000
     C* set array element pointer for procedure name
     C               SETOF             90
     C               Z-ADD1      Z      20
     C         @PROC       LOKUPPROC,Z             90
     C N90           DO
     C               SETON             U7    *proc not found - cancel job
     C               GOTO T$9999
     C               END
     C* display 'prompt' screen
     C               EXCPTWK110
     C* output // COPY statement with proc name
     C               EXCPTOCL1
     C* output common begining sort specs
     C               DO    5      X
     C               EXCPTOCL2
     C               END
     C* output sort header spec
     C               EXCPTOCL3
     C* output user-selected record type specs
     C               MOVE *BLANKS   #AND           *clear 'AND' code
     C               DO    18      X
     C         $FN,X       IFGE 1               *assume if valid $FN
     C         $FN,X       IFLE 18               *   then valid $OP and $CR
     C               SETOF             90
     C               Z-ADD1      Y      20
     C         $OP,X       LOKUPOPNS,Y             90*assume valid OPN since passed EDIT$
     C               EXCPTOCL4
     C               END
     C               MOVE 'A'      #AND           *set 'AND' code for remainder of loop
     C               END
     C* output field records for this proc
     C               DO    28      X
     C               SETOF             90
     C         @PROC       IFEQ PNAM,X
     C               EXCPTOCL5
     C               END
     C               END
     C*==============================================HILOEQ===========
     CSR     EDIT$       BEGSR
     C*
     C               Z-ADD0      #@ERR   10       *clear error flag
     C               DO    18      X      20       .DO once for each screen line
     C         X     ADD    39      Y      20        point to screen indicator
     C               Z-ADD0      *IN,Y         clear screen indicator
     C* if field num is blank, blank out operation and criterion
     C         $FN,X IFEQ *BLANKS
     C         $FN,X OREQ *ZEROS
     C               MOVE *BLANKS   $OP,X
     C               MOVE *BLANKS   $CR,X
     C               GOTO EDITX
     C               END
     C* if operation is blank, blank out field num and criterion
     C         $OP,X IFEQ *BLANKS
     C         $OP,X OREQ *ZEROS
     C               MOVE *ZEROS      $FN,X
     C               MOVE *BLANKS   $CR,X
     C               GOTO EDITX
     C               END
     C* check for valid field number range
     C         $FN,X IFLT 1
     C         $FN,X ORGT 11
     C               Z-ADD1      *IN,Y         turn on corresponding indicator
     C               Z-ADD1      #@ERR         set error flag
     C               END
     C* check for valid operation code
     C               SETOF             90
     C         $OP,X LOKUPOPNS             90
     C N90           Z-ADD1      *IN,Y          turn on corresponding indicator
     C N90           Z-ADD1      #@ERR          set error flag
     C               END                   .END
     C*
     C         EDITX       TAG
     C*
     CSR           ENDSR
     C*==============================================HILOEQ==========
     OPROC    E            OCL1
     O                     23 '// COPY LIBRARY-P,NAME-'
     O                   @PROC     31
     O          E            OCL2
     O                   COM,X     80
     O          E            OCL3
     O                   HDR,Z     80
     O          E            OCL4
     O                   REC,X     80
     O                   #AND      7
     O                   OPN,Y     18
     O                   $CR,X     34
     O          E            OCL5
     O                   FLD,X     80
**   operation codes
EQGEGTLELTNE
**   procedure names
SAL299SAL297SAL296SAL295SAL294SAL293SAL292SAL291
**   table of procedure names & number of sort field specs
SAL29905SAL29703SAL29603SAL29503SAL29403SAL29303SAL29205SAL29103
**   common 'beginning' sort specs
// REGION SIZE-64
// LOAD #GSORT
// FILE NAME-INPUT,LABEL-PRO.SCD,DISP-SHR
// FILE NAME-OUTPUT,LABEL-SAL010?WS?
// RUN
**   header specs for each program
//   HSORTA    17A      3       N           *SAL299
//   HSORTA    18A      3       N           *SAL297
//   HSORTA    18A      3       N           *SAL296
//   HSORTA    5A      3       N           *SAL295
//   HSORTA    18A      3       N           *SAL294
//   HSORTA    18A      3       N           *SAL293
//   HSORTA    8A      3       N           *SAL292
//   HSORTA    18A      3       N           *SAL291
**   common record selection and field specs (insert AND code & test condition)
//   I C   3   8  D                   *customer number
//   I C   9  23  C                   *item number
//   I C  36  36  C                   *sbu
//   I C  37  38  C                   *product group
//   I C 548 548  C                   *product line
//   I C 549 550  C                   *product class
//   I C  39  40  C                   *item class
//   I C  41  42  C                   *customer type
//   I C  43  44  C                   *company code
//   I C  33  34  C                   *sales region
//   I C  35  35  C                   *sales territory
**   proc names and their respective #GSORT field specs
SAL299//   FNC    33  34                     *sales region

Sunday, February 19, 2012

Simple Circles–NHibernate Unit Testing (Part 7c)

This post is part of a series on building a simple to use web-based contact and customer relationship management application. The goal is to support various audiences including businesses, teams, clubs, religious organizations, etc.

In Part 7a I implemented the Repository pattern using NHibernate and in Part 7b I used Fluent NHibernate to implement the Data Mapper pattern between the domain layer and the relational database layer. In this post I’ll build unit tests to exercise these implementations.

One of the problems with attempting to test a data access strategy is ensuring the database is in a known state at the start of each test. Previously I’ve used teardown and setup SQL scripts to empty out data and preload initial values in a database instance. That approach is tedious because it means maintaining two sets of scripts in a different syntax (SQL instead of C#) and using a different toolset (SSMS). Another approach that’s becoming more popular and prevalent is to use a lighter footprint database engine capable of running in-memory. In fact, Fluent NHibernate makes this easy with its standard configuration – all it takes is setting up an NHibernate session configured to use the in-memory database as shown here:

SessionFactory Class
namespace Circles.NHibernateTests
{
  using Circles.NHibernateRepository;

  using FluentNHibernate.Cfg;
  using FluentNHibernate.Cfg.Db;

  using NHibernate;
  using NHibernate.Cfg;
  using NHibernate.Tool.hbm2ddl;

  public class SessionFactory
  {
    private static Configuration staticConfig;

    public static ISessionFactory CreateSessionFactory()
    {
      return
        Fluently.Configure()
          .Database(SQLiteConfiguration.Standard.InMemory().ShowSql())
          .Mappings(m => m.FluentMappings.AddFromAssemblyOf<PartyRepository>().ExportTo(System.Console.Out))
          .ExposeConfiguration((c) => staticConfig = c)
          .BuildSessionFactory();
    }

    public static void BuildSchema(ISession session)
    {
      var export = new SchemaExport(staticConfig);
      export.Execute(script: true, export: true, justDrop: false, connection: session.Connection, exportOutput: null);
    }
  }
}
Lines 20-22 contain all the “magic”: line 20 configures the database to use the standard in-memory configuration for SQLite. Line 21 sets up the mappings to be read from the PartyRepository assembly; in addition, the ExportTo() method call will dump the generated hbm.xml to the console. Line 22 exposes the configuration being set up before it is passed to NHibernate for session creation. In this case, the Lambda expression stores the newly built configuration in a static variable for later reuse by the BuildSchema method.

xUnit does not implement or use constructs such as [Setup] / [Teardown] like nUnit does or [TestInitialize] / [TestCleanup] like MSTest does since the authors feel it is problematic. You can read more about the rational on James Newkirk’s blog post “Why you should not use Setup and TearDown in NUnit”. Pretty compelling when the main author himself tells you not to; however, I’m going to bend the rules a little. I’ve implemented a base testing class that contains a virtual method called TestInitialize() whose purpose is to prepare the testing database and put it in a known state before each test is executed:

TestBase Class
namespace Circles.NHibernateTests
{
  using NHibernate;

  /// <summary>
  /// Base class for NHibernate / SQLite tests
  /// </summary>
  public abstract class TestBase
  {
    protected static readonly object LockObject = new object();

    private static ISessionFactory sessionFactory;

    protected ISession Session { get; set; }

    /// <summary>
    /// Code to run before the test to allocate 
    /// and configure any resources needed.
    /// </summary>
    public virtual void TestInitialize()
    {
      sessionFactory = SessionFactory.CreateSessionFactory();
      Session = sessionFactory.OpenSession();
      SessionFactory.BuildSchema(Session);
    }

    public void Dispose()
    {
      Session.Dispose();
    }
  }
}

Each test class can now easily prepare an in-memory database and place it in a known state prior to executing tests by overriding TestInitialize() with its own specific behavior:

TestInitialize() Method
public override void TestInitialize()
{
  Monitor.Enter(LockObject);

  base.TestInitialize();

  using (var tx = Session.BeginTransaction())
  {
    var householdType = new PartyType(new Guid(Constants.PartyTypeIdHousehold))
    {
      TypeName = "Household"
    };
    Session.Save(householdType);

    var personType = new PartyType(new Guid(Constants.PartyTypeIdPerson))
    {
      TypeName = "Person"
    };
    Session.Save(personType);

    var person = new Person
    {
      FirstName = "Nancy",
      LastName = "Davolio",
      Gender = GenderEnum.Female,
      Birthday = new DateTime(1948, 12, 8),
      PartyType = personType
    };
    Session.Save(person);

    Session.Flush();
    tx.Commit();
  }

  Session.Clear();

  Monitor.Exit(LockObject);
}

The Monitor.Enter and Monitor.Exit calls (lines 3 & 38) ensure that access to the NHibernate Session remains serialized and two test methods can’t accidently interfere with each other in multi-threaded harnesses. The first call is to the base class’ implementation which creates the in-memory database. Following that I create PartyType instances for Household and Person and then create and persist a well-known Person instance.

With the base class and SessionFactory implemented and in place, constructing unit tests that can exercise (indirectly) the mappings as well as an actual, deployed database layer becomes trivial:

CanAddHousehold() Test Method
[Fact]
public void CanAddHousehold()
{
  this.TestInitialize();

  IPartyRepository repository;
  Household household;

  // arrange
  using (var tx = Session.BeginTransaction())
  {
    repository = new PartyRepository(Session);
    var partyType = repository.FindPartyTypeById(new Guid(Constants.PartyTypeIdHousehold));

    // act
    household = new Household
    {
      PartyName = "Eisenhower",
      FormalGreeting = "President Dwight D. and Mrs. Mamie",
      InformalGreeting = "Ike and Mamie",
      PartyType = partyType
    };

    repository.Add(household);
    tx.Commit();
  }

  // assert
  Assert.NotNull(household);
  Assert.NotNull(repository.FindById(household.PartyId));
}

Line 4 initializes the database including preparing the known state, Line 12 initializes a PartyRepository instance using the in-memory database session. This particular line of code deserves special attention – I’m creating a PartyRepository and giving it the in-memory database to operate against. Recall in Part 7a on Line 47 of the fourth code fragment (DBSession class)  that a PartyRepository instance was created using the “real” NHibernate DbSession there. Here’s where designing the NHibernate repository to accept an ISession implementation gives flexibility.

Line 13 retrieves the household party type that is known to be there because it was created and saved during TestInitialize(). Finally the household is created and saved. An interesting side effect of this test was when I first ran it – it failed. I had forgotten to specify/assign a PartyType to the household instance. When I referred back to the earlier tests that I put together using the FakeRepository I realized that it too wasn’t assigning or checking the party type. This oversight illustrates a problem with trying to fake or mock an implementation – you have to remember to code and test all the validation and business rules necessary to be a “real” repository. This is the best reason to have taken these steps of setting up a reusable, fast way to test the real repository – as you code the repository, you can test it without having to write double the code by updating the fake one.

This discovery led to adding a negative test to ensure the repository doesn’t allow a household without a party type:

Negative Test
[Fact]
public void CanNotAddHouseholdWithMissingPartyType()
{
  this.TestInitialize();

  // arrange
  using (var tx = Session.BeginTransaction())
  {
    var repository = new PartyRepository(Session);

    // act
    var household = new Household
    {
      PartyName = "Eisenhower",
      FormalGreeting = "President Dwight D. and Mrs. Mamie",
      InformalGreeting = "Ike and Mamie",
      /* PartyType = partyType */
    };

    // assert
    Assert.Throws<NHibernate.PropertyValueException>(
      delegate
      {
        repository.Add(household);
      });

    tx.Rollback();
  }
}

Lines 21-25 uses the xUnit Assert.Throws<> method to wrap the attempt to add a household without a party type. If the repository doesn’t throw an NHibernate.PropertyValueException then the test is considered to have failed.

This post was pretty densely packed with concepts - an entire framework for testing an NHibernate-based data layer. The accompanying code base is growing larger and can be downloaded from here.

Friday, February 3, 2012

Simple Circles–NHibernate Mapping (Part 7b)

This post is part of a series on building a simple to use web-based contact and customer relationship management application. The goal is to support various audiences including businesses, teams, clubs, religious organizations, etc.
In a previous post, I used AutoMapper to implement the DataMapper pattern to map and flatten the domain model into view models for the MVC user interface. The same approach applies to mapping the domain model to a relational database. In fact, this is the heart of an object-relational mapping (ORM) tool like NHibernate. Originally, NHibernate used XML-based mapping files similar to its Hibernate lineage. More recently, James Gregory’s Fluent NHibernate tool has emerged as a leading alternative with strengths such as supporting the fluent interface programming style, convention-based mappings, and compile-time checking (a huge time saver).
Simple Circles - Install FluentNHibernateAfter adding Fluent NHibernate v1.2.0.712 – that version is compiled against the NHibernate version I’m using – I created a \Maps folder and created the mapping classes. Before diving into the mapping, it’s time to introduce a best practice for managing concurrency and allowing detection of stale objects. By convention, NHibernate supports a special-named property called ‘Version’ that can be numeric (preferred), a timestamp or a DB timestamp. You can find more information at the NHibernate site or Ayende’s post. In the Circles data model, I’ve got a base Entity class which is a great place to put the Version property so every Entity automatically gets the benefit of optimistic concurrency checking. Here's the mappings for Party and PartyType:
namespace Circles.NHibernateRepository.Maps
{
  using Circles.Domain;

  using FluentNHibernate.Mapping;

  public class PartyMap : ClassMap<Party>
  {
    public PartyMap()
    {
      Id(x => x.PartyId).GeneratedBy.GuidComb();
      Version(x => x.Version);
      References(x => x.PartyType, "TypeId")
          .Not.Nullable();
      Map(x => x.PartyName)
          .Not.Nullable()
          .Length(255)
          .Index("ukPartyName");
    }
  }

  public class PartyTypeMap : ClassMap<PartyType>
  {
    public PartyTypeMap()
    {
      Id(x => x.TypeId).GeneratedBy.GuidComb();
      Version(x => x.Version);
      Map(x => x.TypeName)
          .Not.Nullable()
          .Length(255)
          .Index("ukTypeName");
    }
  }
}

Line #11 above maps the Id property to a column called PartyId and specifies that the Guid.Comb Identifier Strategy be used to generate new ids. Lines #12 and #13 map the referenced PartyType within a Party by creating a foreign key using TypeId as the column name.

Fluent NHibernate has the ability to not only define mappings but to configure NHibernate using the mappings along with other settings you specify. Using this ability, its also possible to export the configuration to .hbm.xml files combined with NHibernate’s ability to export .hbm files to SQL DDL. There’s a lot of power packed into the following:
namespace NHibernateSchemaExport
{
  using Circles.NHibernateRepository;

  using FluentNHibernate.Cfg;
  using FluentNHibernate.Cfg.Db;

  using NHibernate.Cfg;
  using NHibernate.Tool.hbm2ddl;

  class Program
  {
    static void Main(string[] args)
    {
      Fluently.Configure()
        .Database(SQLiteConfiguration.Standard
            .ConnectionString(c => c.FromConnectionStringWithKey("circlesdb"))
            .ShowSql)
        .Mappings(m => m.FluentMappings.AddFromAssemblyOf()
            .ExportTo("."))
        .ExposeConfiguration(BuildSchema)
        .BuildSessionFactory();
    }

    static void BuildSchema(Configuration cfg)
    {
      new SchemaExport(cfg)
             .SetOutputFile("CirclesSchema.sql")
             .Execute(script: true, export: false, justDrop: false);
    }
  }
}

Line #18 shows the ExportTo() method being using to export the configured mappings to the current directory. Line #19 uses the ExposeConfiguration() method along with NHibernate’s SchemaExport() method on line #26 to output the database schema. These techniques give us the ability to “see into” the dynamically generated hbm and sql files.

There’s more details in the accompanying change set for this post that can be downloaded here. Next up is implementing the PartyRepository now that NHibernate is configured and the domain-to-database mapping is coming together.

Sunday, January 29, 2012

Simple Circles–NHibernate Repository (Part 7a)

This post is part of a series on building a simple to use web-based contact and customer relationship management application. The goal is to support various audiences including businesses, teams, clubs, religious organizations, etc.
I’ve got a decent foundation in place with the domain model, persistence interface defining the repository pattern and a concrete implementation with a fake repository for testing purposes. The fake repository also serves to easily and quickly validate the design – if it is difficult to implement with simple in-memory structures then it’ll likely be even harder using an ORM. I have more experience with NHibernate and it’s a more sophisticated and mature ORM so I’ll start with it first.
To begin with, I’ve added two interfaces to the Persistence class library to define a database session and a factory pattern to manage creating sessions.
namespace Circles.Persistence
{
  using System;
  using System.Data;

  public interface IDbSession : IDisposable
  {
    void BeginTransaction(IsolationLevel isolationLevel);

    void Commit();

    void Rollback();

    IPartyRepository CreatePartyRepository();
  }
}
Bob Cravens provided inspiration for the session work with his Truck Management System. that eventually made it into his own GenericRepository project. This database session interface specifies that concrete implementations provide BeginTransaction, Commit, and Rollback methods as well as a concrete implementation for creating a PartyRepository.
The DbSessionFactory defines the factory pattern with a little bit of flexibility – you can create a DbSession with or without transactional support.
namespace Circles.Persistence
{
  public interface IDbSessionFactory
  {
    IDbSession Create(bool withTransaction);
  }
}
Simple Circles - Install NHibernateNext I created an NHibernateRepository class library project and added the NHibernate package via NuGet. While NuGet makes it simple to install packages and their dependencies, it doesn’t relieve you from managing versions. It is usually best to specify an explicit version when installing packages to ensure you don’t get conflicts and other surprises. In my case I used “-version 3.1.0.4000” which also installs the same version of Iesi.Collections.
The implementation of DbSessionFactory is pretty lightweight and trivial:
namespace Circles.NHibernateRepository
{
  using System.Data;
  using Circles.Persistence;

  public class DbSessionFactory : IDbSessionFactory
  {
    public IDbSession Create(bool withTransaction)
    {
      DbSession session = new DbSession(SessionProvider.SessionFactory);

      if (withTransaction)
      {
        session.BeginTransaction(IsolationLevel.ReadCommitted);
      }

      return session;
    }
  }
}

The SessionProvider class used in line #10 above will be discussed below. DbSession is also lightweight because it delegates the heavy lifting to the underlying NHibernate methods:
namespace Circles.NHibernateRepository
{
  using System;
  using System.Data;
  using Circles.Persistence;
  using NHibernate;

  public class DbSession : IDbSession
  {
    private readonly ISession session;
    private ITransaction transaction;

    public DbSession(ISessionFactory sessionFactory)
    {
      if (sessionFactory == null)
      {
        throw new ArgumentNullException("sessionFactory");
      }

      this.session = sessionFactory.OpenSession();
      this.session.FlushMode = FlushMode.Auto;
    }

    ~DbSession()
    {
      this.Dispose();
    }

    public void Dispose()
    {
      if (this.session == null)
      {
        return;
      }

      lock (this.session)
      {
        if (this.session.IsOpen)
        {
          this.session.Close();
        }
      }

      GC.SuppressFinalize(this);
    }

    public IPartyRepository CreatePartyRepository()
    {
      return new PartyRepository(this.session);
    }

    public void BeginTransaction(IsolationLevel isolationLevel)
    {
      this.transaction = this.session.BeginTransaction(isolationLevel);
    }

    public void Commit()
    {
      if (this.transaction == null)
      {
        return;
      }

      if (!this.transaction.IsActive)
      {
        throw new InvalidOperationException("No active transation");
      }

      this.transaction.Commit();
    }

    public void Rollback()
    {
      if (this.transaction == null)
      {
        return;
      }

      if (this.transaction.IsActive)
      {
        this.transaction.Rollback();
      }
    }
  }
}

NHibernate is highly configurable so instantiating the libraries requires processing configuration settings contained in hibernate.cfg.xml as well as reflecting over your assemblies. Doing so is expensive – its best to cache the initialized configuration and session for the lifetime of the application. The SessionProvider class does this:
namespace Circles.NHibernateRepository
{
  using NHibernate;
  using NHibernate.Cfg;

  public class SessionProvider
  {
    private static Configuration configuration;

    private static ISessionFactory sessionFactory;

    private SessionProvider()
    {
    }

    public static Configuration Configuration
    {
      get
      {
        if (configuration == null)
        {
          configuration = new Configuration();
          configuration.Configure();
          configuration.AddAssembly(typeof(PartyRepository).Assembly);
        }

        return configuration;
      }
    }

    public static ISessionFactory SessionFactory
    {
      get { return sessionFactory 
              ?? (sessionFactory = Configuration.BuildSessionFactory()); }
    }

    public static ISession GetSession()
    {
      return SessionFactory.OpenSession();
    }
  }
}

That’s a lot of implementation and setup plumbing! The source code for this article can be downloaded from here. The next installment will map the domain model to the relational database model.

Sunday, January 22, 2012

Simple Circles–Enabling jQuery UI (Part 6)

This post is part of a series on building a simple to use web-based contact and customer relationship management application. The goal is to support various audiences including businesses, teams, clubs, religious organizations, etc.
I think of the default blue MVC 3 UI as something of a developer’s test version – not really what you’ll go to production with but sufficient to work with while developing. It’s possible to use the ASP.NET MVC themes from their gallery but I think jQuery and its companion jQuery UI provide a much richer and cross-browser solution with far less effort.

Themes

Steve Hobbs wrote a great step-by-step post on enabling jQuery UI last June. Like him, I used the Redmond theme to start with so it more closely resembled the default.
Simple Circles - Enable jQuery UISimple Circles - jQuery UI themeAfter downloading the Redmond theme, copy the css\redmond folder to the Content\themes folder under the Circles website. Remember to choose “Include in Project” from the Solution Explorer (see image to the left). As Steve Hobbs indicates, jQuery UI support is already included in the ASP.NET MVC 3 templates – it just needs to be enabled by adding two lines to the _Layout.cshtml in the Shared folder as shown on the right. To start taking advantage of the jQuery UI theme, simply add the appropriate styles to HTML elements. Here I’ve commented out the standard Submit button on the Create.cshtml page and replaced it with one decorated with jQuery UI CSS classes:
<!--<input type="submit" value="Create" />-->
<input type="submit" value="Create" 
       class="ui-button ui-widget ui-state-default ui-corner-all" 
       role="button" aria-disabled="false">

Simple Circles - jQuery buttonThis screen shot shows the before and after using the Redmond theme just set up. These CSS classes are designed such that switching themes is as easy as deploying a different theme to the Content\themes folder and changing the single <link> line in the _Layout.cshtml page. The “eye candy” is nice – it looks fresher and more modern…well, months ahead of the default hard images the browser displays!

Next I’m going to add some feedback for the user when an action completes. The default behavior when adding, updating or deleting an entity is to perform the action then return the user to the Index page. Breaking this down, it means the controller performs the action then uses RedirectToAction to redirect the browser to the default HTTP GET action. Doing so ensures that the user can’t accidently use refresh to repeat the same action, e.g. adding the same entity twice or, classically, buying the same thing again.

Because redirect is used the current HTTP request ends and another one initiates. This means the feedback message has to be passed across HTTP requests so the usual suspect – ViewData – won’t work. The MVC engineers planned for this with another dictionary object that’s briefly stored in the user’s session – TempData. The MSDN article Passing Data in an ASP.NET MVC Application provides the necessary details. Here I tuck the confirmation message into TempData (line #4) before issuing the redirect:
  ...
  this.repository.Add(entity);

  TempData["message"] = string.Format("{0} was added", entity.PartyName);

  return RedirectToAction("Index");
}

Back in the Index.cshtml the message just passed to it is retrieved and rendered with jQuery UI classes decorating the output:
<h2>People</h2>

@if (TempData["message"] != null) {
  <div style="margin-top: 20px; padding: 0 .7em;" class="ui-state-highlight ui-corner-all"> 
    <p><span style="float: left; margin-right: .3em;" class="ui-icon ui-icon-info"></span>
      @TempData["message"]
    </p>
  </div>
}

Simple Circles - ConfirmationThe result is shown to the right. Another simple feature to take advantage of jQuery’s client-side power is to enable “unobtrusive client-side validation”. There’s plenty of examples already, including the ASP.NET Teams’s own tutorial. Two web.config settings and two more <link> elements to include necessary JavaScript files in _Layout.cshtml and field/form validation moves from server-side down to client-side to provide feedback without having to HTTP POST the form to the server then getting the validation messages sent back to the client browser.

Menu Tabs and Lists

Simple Circles - Tabbed MenuOnce again I’m turning to the ASP.NET team’s tutorials to improve the user experience even more. Their Iteration #2 – Make the application look nice article contains two enhancements; one improves the menu tabs by rounding the corners and providing highlighting to indicate the selected tab the user is on and the other replaces the textual hyperlinks on each row with graphics for edit and delete. Simple Circles - Nicer ListsAgain, rather than duplicating details here, please refer to the article for more information. The result of these efforts is shown on the right.

The source code for this article can be downloaded from here. The next several installments will flesh out concrete implementations of the repository using Entity Framework and NHibernate.

Thursday, January 19, 2012

Simple Circles–MVC Controller Unit Tests (Part 5)

This post is part of a series on building a simple to use web-based contact and customer relationship management application. The goal is to support various audiences including businesses, teams, clubs, religious organizations, etc.

Simple Circles - ControllerTestsIn the last post I left off with the PeopleController refactored to use an IPartyRepository via dependency injection. Now I’ll create unit tests for the controller so that we’ll know if something breaks as the solution evolves. Since I’m planning on several views and controllers, I added a ControllerTests folder to the UnitTests project to organize and group them together. With xUnit, as with many unit testing frameworks, simply add a new class called PeopleControllerTest and begin writing the tests.

Unit testing MVC controllers generally falls into two categories:
  1. Ensuring that an action result being returned from a controller’s action is correct. On HTTP GET requests, this means the correct view result is returned.
  2. Ensuring that HTTP POSTs are behaving properly:
    1. A POST with purposely invalid model data causes it to route the request back to the originating view.
    2. A POST with correct model data causes it to route back to the Index view.
Because of the dependency injection work just done, setting up the unit test is straightforward:
[Fact]
public void DefaultGetReturnsIndexView()
{
// arrange
const string ExpectedViewName = "Index";
var peopleController =
new PeopleController(new FakeRepository.FakePartyRepository());

// act
var viewResult = peopleController.Index();

// assert
Assert.NotNull(viewResult);
Assert.Equal(ExpectedViewName, viewResult.ViewName);
var model = viewResult.ViewData.Model as IEnumerable;
Assert.NotNull(model);
}

Notice on Line #6 above that it’s easy to pass a new instance of the FakeRepository from the test method. The assertions section checks that a view result was returned, that it was the expected named view result and that the view’s model is present.


Testing the HTTP POST actions is a bit more involved. The basic test is the same however since the testing class is calling the controller directly a little more work is needed. Model validation is a part of the MVC runtime which is not present during testing so it is necessary to “prime the pump” so to speak by placing the error into the controller’s ModelState to see if it’s being handled correctly – this is essentially the same behavior the MVC runtime exhibits:

[Fact]
public void CreatePostReturnsViewIfModelStateIsInvalid()
{
// arrange
const string ExpectedViewName = "Create";
var peopleController
= new PeopleController(new FakeRepository.FakePartyRepository());
peopleController.ModelState.AddModelError("LastName", "LastName is required.");
var person = new Person
{
FirstName = "Henry",
MiddleName = "Lewis",
//LastName = "Stimson ",
Gender = Enum.GetName(typeof(GenderEnum), GenderEnum.Male),
Birthday = new DateTime(1867, 9, 21)
};

// act
var viewResult = peopleController.Create(person) as ViewResult;

// assert
Assert.NotNull(viewResult);
Assert.Equal(ExpectedViewName, viewResult.ViewName);
}

Line #8 above is where the error is set up. Lines #9 through #16 create a Person instance to pass to the controller method to mimic what the MVC runtime will do. Notice that I commented out the line for setting the LastName property for authenticity purposes. Personally, I would hate to come across some else’s code that sets up an error yet passes in perfectly valid data. Finally, the assert statements check that the same Create view is returned indicating that the error(s) were detected and handled properly.

Testing for a valid model is nearly the same – just don’t set the ModelState, pass a valid Person instance and check that we’re returned to the Index page. Edit, update and delete actions are nearly identical as well.


The final bit of code worthy of mention is around the use of AutoMapper – remember that the last post introduced AutoMapper as an implementation of the DataMapper pattern. I wired up the mapping initialization inside Global.asax.cs then. Of course the test harness doesn’t have a website since it’s job is exercising the logic and behavior of the controller classes. Therefore I have to use the xUnit way of introducing a “fixture” necessary for the test class to operate.

public class PeopleControllerTest : IUseFixture<DataMapperFixture>
{
public void SetFixture(DataMapperFixture data)
{
data.CreateMaps();
}
...
}
...
public class DataMapperFixture : IDisposable
{
public void CreateMaps()
{
Mapper.AssertConfigurationIsValid();
PersonMap.CreateMaps();
}
}

The test class implements the xUnit IUseFixture<T> interface which tells the xUnit runtime that it needs to call SetFixture with an instance of that type – in my case I created a trivial DataMapperFixture class that simply verifies the mapping is valid and calls the PersonMap.CreateMaps() method to initialize AutoMapper.


The source code for this article can be downloaded from here. The next installment will explore some simple jQuery enhancements to make the UI a little nicer and more informative.