/* 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;
Seeking a pragmatic approach to software development blending proven designs with real-world constraints.
Wednesday, May 2, 2012
Extracting MSDS Finished Goods from MAPICS/38 using PL/1
Code Generation circa 1988 using RPG II 1/2
- 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.
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)
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)
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).
After 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)
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);
}
}
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)
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.
After 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">
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>
}
Menu Tabs and Lists
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)
Unit testing MVC controllers generally falls into two categories:
- 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.
- Ensuring that HTTP POSTs are behaving properly:
- A POST with purposely invalid model data causes it to route the request back to the originating view.
- A POST with correct model data causes it to route back to the Index view.
[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.