Skip Navigation LinksTech Blogs

  • Test-Driven Development in Natural

    by Marc Moore | May 22, 2013

    Is TDD/automated unit testing possible in Natural? On one hand, Natural is a programming language like any other. Writing truly modular code is indeed possible. On the other, doing so is far from frictionless.

    Basic TDD requires that external dependencies - databases, screens, etc. - be isolated from the code under test.  That means that testable Natural code cannot be part of a program/sub-program that contains database access code or screen-related code.  Nor are sub-routines discrete, testable units because of their inherent reliance on global variables.  Practically speaking, Natural sub-programs are the only candidates for TDD and only those sub-programs that have no environmental dependencies.

    For an example of a TDD-ready Natural sub-program, consider HNSPMASK, a utility program that masks sensitive data fields.  HNSPMASK has no database or UI dependencies and accepts all of the information it needs to fulfill its purpose through its parameter area:

    0010 ************************************************************************
    0020 * PROGRAM NAME      : HNSPMASK
    0140 ************************************************************************
    0150 *
    0160 DEFINE DATA
    0170 PARAMETER USING HNDPMASK
    0180 *

    HNDPMASK is presents a simple interface, but it's easy to imagine a sub-program whose interface is much more complex:

    0010 DEFINE DATA PARAMETER
    0020  1 #HNDPMASK-PARAMETERS (A256)
    0030  1 REDEFINE #HNDPMASK-PARAMETERS
    0040    2 #HNDPMASK-NBR-UNMASKED (I2)
    0050    2 #HNDPMASK-DATA-STR (A253)
    0060    2 #HNDPMASK-DIRECTION (A1) /*R or L
    0070 END-DEFINE 

    Calling HNSPMASK is also simple:

    2820 MOVE 4 TO #HNDPMASK-NBR-UNMASKED
    2830 MOVE 'L' TO #HNDPMASK-DIRECTION
    2890 MOVE #MY-DATA TO #HNDPMASK-DATA-STR
    2900 CALLNAT 'HNSPMASK' #HNDPMASK-PARAMETERS
    2910 MOVE #HNDPMASK-DATA-STR TO #MY-DATA

    Nothing unusual so far, right? Indeed, there is nothing unusual about HNSPMASK at all, other than the fact that a test driver named HBTDMASK was written to prove its functionality (abbreviated below):

    0010 ***********************************************************************
    0020 * PROGRAM      : HBTDMASK
    0060 ***********************************************************************
    0080 DEFINE DATA
    0100 LOCAL USING HNDPMASK
    0120 LOCAL
    0200 1 #TEST-4-CHAR-INPUT                  (A253) CONST<'ABCD'>
    0220 1 #TEST-L2R-4-CHAR-MASK-2-EXPECTED    (A253) CONST<'**CD'>
    0320 END-DEFINE
    0330 *
    0380 MOVE 'L' TO #HNDPMASK-DIRECTION
    1130 *
    1140 * 4-char string should return same string
    1150 * with a 2-char mask size
    1160 *
    1170 MOVE 2 TO #HNDPMASK-NBR-UNMASKED
    1180 MOVE #TEST-4-CHAR-INPUT TO #HNDPMASK-DATA-STR
    1190 CALLNAT 'HNSPMASK' #HNDPMASK-PARAMETERS
    1200 IF #HNDPMASK-DATA-STR EQ #TEST-L2R-4-CHAR-MASK-2-EXPECTED
    1210   PRINT '4-char, mask 2 test passed'
    1220 ELSE
    1230   PRINT '4-char, mask 2 string test failed:'
    1240     ' Expected "' #TEST-L2R-4-CHAR-MASK-2-EXPECTED
    1250     '"; received "' #HNDPMASK-DATA-STR '"'
    1260 END-IF
    1270 *

    Many additional scenarios were devised in HBTDMASK, but the redacted code above illustrates the point.  We know from the tests that HNSPMASK functions as specified and, if it is modified, the tests will demonstrate that it still works or show that the change has broken existing functionality.

    As written, HBTDMASK could be executed in the Test environment via via JCL and results forwarded to the development team as needed.

    Another option would be to create a project-wide test driver program - call it HBTDD, for example - and collect all of an application's Natural-based TDD calls into a single program.

    A third option would be to move the tests into a Natural procedure called via Shadow.  In this scenario, the tests could be integrated into the normal C# unit tests, at the cost of binding these tests to a Shadow ODBC connection.

    This is a simple example, but imagine a complex payroll or benefit calculation sub-program that was entirely free of external dependencies.  All calculations could be performed inside a highly modular, highly testable sub-program.  A suite of unit tests could then be created to prove the calculations and these tests would then provide regression tests against all future changes to the calculations.  The result is that fear of making changes to the calculation is eliminated, making the team more agile and responsive.

  • Creating Psuedo-Tables in Adabas

    by Marc Moore | Apr 16, 2013

    Many of our older applications use Adabas as the backend database. Those applications often use BPP-TABLES to store their lookup/reference data in pseudo-tables.

    BPP-TABLES layout with the important fields/super-descriptors highlighted:

    BPP-TABLES Layout

    Creating a table is a simple process.  First, log into the mainframe, then go to the BPP Test environment.  From the Command prompt, enter M to launch the BPP Test UI.

    Launch BPP Test UI

    Next, enter 451 in the Screen field and press Enter to get to the GENERIC TABLES screen.

    To add a new table, enter an A in Function column of the first row, then enter the following data values:

    • Key field – your pseudo-table name
    • Maint. on 451 field – Y 
    • Data field – your table description

    For example:

    Add a Table to BPP-TABLES

    Hit Enter to create your table.  You should receive a message like “0202 Record has been successfully added” from BPP.

    Next, enter the first few characters of the new pseudo-table’s key into the Start From field and press Enter:

    Search BPP-TABLES

    Next, enter an X into the Function column of your pseudo-table’s row and press Enter:

    Add Data to BPP-TABLES

    To enter your lookup records, enter an A into the Function column of each row you want to add along with the Key and Data values for the row(s):

    Add Rows to BPP-TABLES

    Press Enter to save your data into the database.  You should receive another “0202 Record has been successfully added” message from BPP.

  • Creating Lookup Data From a C# Class

    by Marc Moore | Apr 08, 2013

    Hard-coding constant values in C# classes is a bad practice that we rarely indulge in here at TAMUS ESI.  You will not see very many instances of code like:

        if (someObject.StatusCode == "A")

     We prefer something more like:

        if (someObject.StatusCode == SomeProjectConstants.SomeFieldStatus.Active)

    where SomeFieldStatus is a class like this one in HRConnect 2:

        public static class BppLogFieldCodes         
    {            
    public const string FIT = "FIT";
    public const string ACH = "ACH";
    ...
    }

     

    Nothing fancy about this, but creating classes/constants like these takes only moments and helps reduce bugs and makes changing/adding new codes easier in future releases.

    What isn't so helpful about this class - or any other basic C# class - is that it doesn't provide any metadata about the code values. For example, it would be very convenient if the "friendly description" for the FIT code were directly associated with the code property in the BppLogFieldCodes class.

    The usual technique for getting code/description translation data into an ESI web application is to use a "Lookups" class that knows how to connect to a data source, read a database table (or tables), and create a list of key/value pairs.  The Lookups class will also normally provide caching of the resulting queries to improve performance.  The Lookup data is then used to populate dropdowns, radio button lists, etc.  Again, nothing fancy.

    Such Lookup classes are all well and good, but having cached lookup data does nothing to eliminate the BppLogFieldsCode class and others like it - the constants are still needed for comparisons, etc., as initially demonstrated.

    But what if we could create a class like this one that includes the lookup metadata in the class definition?

        public static class BppLogFieldCodes         
    {            
    [Description("Federal Income Tax")]            
    public const string FIT = "FIT";

    [Description("Direct Deposit")]            
    public const string ACH = "ACH";
    ...
    }

    Thanks to the new AttributeInterrogator class in the So.Esi.Core assembly, it is now possible to annotate your class as shown, then write a simple, one-line method to get a list of lookup data for use in your app's UI.  For example:

        public IList<LookupDataDTO> GetBppLogFieldSets()         
    {            
    return MapAttributeListToLookupData(                
    AttributeInterrogator.GetTypeAttributes<DescriptionAttribute>(
    typeof(HRConnectWebConstants.BppLogFieldCodes)));        
    }

     

    The GetBppLogFieldSets method can be used in exactly the same way as methods in the "Lookups" data wrapper class are used now, but no database connection/access is required.

    So how is this accomplished? 

    The So.Esi.Core assembly contains a DescriptionAtttribute class that inherits from System.Attribute and accepts a description as a parameter to its constructor.  That allows the [Description] attribute to be applied to a class and its properties. 

    The Description attribute can also be applied to a C# enum and its fields.  To make enums even more descriptive, a CodeDescription attribute was created to handle the "three-way" mapping that often takes place with an enumeration. 

    In the three-way enum scenario, the three values in play are:

    1. The enum value itself
    2. The lookup code
    3. The friendly description

    For example, consider the following enum modeled on TrainTraq 3:

        [Description("Role")]         
    public enum CodedRatingEnum
    {
    [CodeDescription("APP-EMPLOYEE", "Employee")]
    Employee = 1,

    [CodeDescription("APP-DEPT-ADM", "Department Admin")]
    Good = 2,
    ...
            }

     

    The AttributeInterrogator class helps make this metadata convenient to consume.  It also allows developers to create their own custom metadata attribute classes and apply them in their applications.

    A quick look at the current implementation of the GetPropertyAttributes method, which is used to scan a class for its metadata, demonstrates how it works beneath the covers:

        public static IList<AttributeInformation<T>> GetPropertyAttributes<T>(object source) 
    where T : System.Attribute
    {
    IList<AttributeInformation<T>> result = null;

    Type sourceType = source.GetType();
    MemberInfo[] members = sourceType.GetMembers();
    foreach (MemberInfo member in members)            
    {       
              if (member.MemberType == MemberTypes.Property)
    {
                    object[] attrs = member.GetCustomAttributes(typeof(T), false);
                    if (attrs != null && attrs.Length > 0)
                    {
    if (result == null)
                            result = new List<AttributeInformation<T>>();

    object value = sourceType.GetProperty(member.Name).GetValue(source, null);
                        result.Add(new AttributeInformation<T>
    {
    Name = member.Name,
    Value = value,
    Attributes = attrs[0] as T
    });
                    }
    }
            }            
    return result;
    }

     

    AttributeInformation is a simple class in So.Esi.Core:

        public class AttributeInformation<T> 
    where T : class
    {
    public string Name { get; set; }
            public object Value { get; set; }
            public T Attributes { get; set; }
    }

     

    It is notable only for the Attributes property, which is generic.  This is what allows application developers to define their own custom attribute classes for use with the AttributeInterrogator.

    The AttributeInterrogator.GetPropertyAttributes method uses reflection to iterate over the specified class' properties.  It then interrogates over each property to see if a custom attribute of the desired type T is defined.  If so, the property name, value, and attribute information is captured in the result list and returned. 

    Similar approaches are taken for interrogating class types and enums.

  • Importing Adabas Data into SQL Server

    by Marc Moore | Mar 26, 2013

    As SQL developers we occasionally need quick access to data that is stored in Adabas. Formal SSIS processes work fine for production use, but in development mode, Adabas data isn't always easy to get because of issues connecting to the mainframe.  Recently I began to use SQL Server's Import and Export for this purpose and it worked well, once I worked the kinks out of the connection process.

    Choose a Data Source

    To copy a table from Adabas to SQL Server using  the SQL Server Import and Export Wizard, first choose the .NET Framework Data Provider for ODBC as the connector type for the data source.

    Next, configure the OBDC connection as follows, filling in the blanks I've excluded from the screen shot for security reasons:

     

    You'll have to fill in the blanks on your own, but here are some hints:

    • You should already have a suitable DSN configured on your development machine, one that uses your RACF credentials to connect to Shadow.
    • Even so, you'll want to supply your "uid" and "pwd" here, as per the DSN
    • The "host" and "port" can also be derived from the DSN

     Choose a Destination

    NOTE: Given that we're going to be copying a table down to SQL, you'll want to create that table in your destination SQL Server database now, if you've not already done so.

    The destination connection type will be SQL Server Native Client.  Point the connection to SQL Server and specify the target database.

    Specify Table Copy of Query

    Because of Shadow's peculiarities, you must choose "Write a query..." and type in the SQL you want to execute to fetch the data from Adabas.  This should be a simple query in the form of:

     SELECT FIELD_1, FIELD_2, FIELD_N FROM TABLE_NAME

    Note the explicit use of underscore characters. While Adabas uses dashes as delimiters, Shadow forces the use of underscores.

    Select Source Tables and Views

    By default, the wizard (stupidly) defaults the Destination to your query, so be sure to select the correct destination table from the non-obvious Destination dropdown.

    Then, with the Source/Destination row highlighted, click the Edit Mappings button and match the field names in your ad hoc SQL query to the columns in your SQL Server table.

     

    Wrap Up

    That's basically it. Click through the rest of the wizard and run your import. 

    If you get a "dynamic mapping" error, you may have tried to import the table directly rather than writing your own SQL, which does not work.

    If you get a security error, the RACF ID you specified may not have access to query the Adabas tables directly.

    The option exists to save the import as an SSIS package, which could be very helpful if you need to repeat the operation.

  • Visual Studio Missing Report Data Window

    by Marc Moore | Oct 18, 2012

    In Visual Studio 2008 – still our SSRS report editing tool of choice – the Report Data panel often “goes missing” and cannot be restored through the Visual Studio menu. 

    The MKC - Magic Key Combination - to restore it is Control+Alt+D.

    image 

  • SSO Build Issues

    by Marc Moore | Aug 06, 2012

    Since moving SSO to the new server, there have been issues getting the application started again after the build is complete.  Symptoms include:

    • SSO not responding to requests for the logon page
    • The PeopleAdmin handler not responding to requests for authentication

    At this time we do not have a solution to the problem, but there is a simple work-around:  Restart SSO’s app pool after the build.  This seems to cure both situations.

    Moreover, after a build it would be prudent to verify the PeopleAdmin handler.  To do this, hit the following URLs in a browser or Fiddler:

     

  • Developer-level Change to SOCommon

    by Marc Moore | Aug 02, 2012

    Our SOCommon database is a read-only database that has commonly-used lookup values, etc., that are used in many of our applications.

    Because SOCommon is so widely used, the change control process for its schema needs to be strictly adhered to.

    Recently the DBA team added a new table/data to the Development instance of the SOCommon database to help with a project I was working on.  After this was done, I:

    1. created a vHRLookupValues view over the new table (the standard for apps querying SOCommon resources requires this)
    2. added the table and view objects to the SOCommon database project in the DEV branch
    3. built the project and created the SOCommon.dbschema file
    4. updated the SOCommon.dbschema file in the GeneratedDbschema folder in the DEV branch

    The first 2 steps are important to keep the database project in sync with the Development database and to keep source control up-to-date.

    Steps 3 and 4 are important to help developers on other projects get the current SOCommon.dbschema file for use in their projects because they should be getting this reference file from the standard location if they need a “hot” change:

    image

    Projects that do not need an up-to-the-minute reference to SOCommon’s schema should get this file from the Prod branch instead.

    At this point I would normally advise the DBA team of the change so they can review the change and handle the promotion to the Training and Prod branches. 

    The above is the essential process for making a change to the SOCommon schema.  Others should follow these steps, at a minimum, to help ensure that the SOCommon database project stays in reasonable shape.

  • Creating an Index on an XML Fragment in SQL Server

    by Marc Moore | Jul 20, 2012

    Imagine you are given a table with a column of XML in it and a few million rows of data.  Further imagine that you want to write an efficient query using an attribute of that XML in the WHERE clause of your query.

    A short test scenario describes how I recently dealt with the problem.  First, a basic table definition.  The XML will end up in the SomeContent field.

    CREATE TABLE [dbo].[MarcTest]
    (
        [ID] [int] NOT NULL,
        [TypeCode] [varchar](20) NOT NULL,
        [SomeContent] [varchar](MAX) NOT NULL
    CONSTRAINT [PK_MarcTest] PRIMARY KEY CLUSTERED
        ([ID] ASC)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    ) ON [PRIMARY]
    GO

    Now for some test data.  As in my real-life scenario, the schema of the XML data varies from row to row.

    INSERT INTO marctest (ID, TypeCode, SomeContent) VALUES(1, 'CODE1', '<MyDoc><MetaData><EmployeeID>13791</EmployeeID><Name>Marc Moore</Name></MetaData><ContentData><ContentField1>This is some content.</ContentField1><ContentField2>This is some additional content.</ContentField2></ContentData></MyDoc>')
    INSERT INTO marctest (ID, TypeCode, SomeContent) VALUES(2, 'CODE2', '<MyDoc><MetaData><EmployeeID>4</EmployeeID><Name>Xi Zhang</Name></MetaData><ContentData><ContentField1>This is some content.</ContentField1><ContentField2>This is some additional content.</ContentField2></ContentData></MyDoc>')
    INSERT INTO marctest (ID, TypeCode, SomeContent) VALUES(1001, 'OTHER1', '<SomeOtherData><Data1>111111</Data1><Data2>222222</Data2>333333<Data3></Data3></SomeOtherData>')
    GO

    It’s not possible to create an index directly on an XML column’s XPath expression and the XML indexes offered by SQL Server 2008 have a bad reputation for being bloated.  So what to do?  The solution that follows was blessed by David, our DBA, as a valid one.

    Based on this question and answer about SQL, XML, and XPath, what I first wanted to do was create a computed column based on an XPATH expression like this one:


    image

    Unfortunately that does not work.  There are a couple of reason for this: The SomeContent column is not an XML column, which doesn’t help, nor does the fact that SQL considers this expression non-deterministic.

    Various articles led me to create this scalar function to help solve the problem:

    FUNCTION MarcTestEmployeeID
    (
        @SomeXml xml
    )
    RETURNS INT
    AS
    BEGIN

        DECLARE @ret INT = @SomeXml.value('(/MyDoc/MetaData/EmployeeID/node())[1]', 'int')
       
        IF @ret IS NULL
            SET @ret = 0
           
        RETURN @ret

    END

    Now SQL was happy and let me create the computed column

    ALTER TABLE MarcTest ADD EmployeeID as ([dbo].[MarcTestEmployeeID]([SomeContent]))

    Unfortunately, when I attempted to create my index:

    CREATE INDEX MarcTestEmployeeID_IDX ON dbo.MarcTest(TypeCode, EmployeeID)

    SQL became unhappy once more:

    Msg 2729, Level 16, State 1, Line 2
    Column 'EmployeeID' in table 'dbo.MarcTest' cannot be used in an index or statistics or as a partition key because it is non-deterministic.

    I’m sure SQL has its own reasons for doing this, but its logic was clearly incorrect as far as my function goes.  Given a blob of XML, dbo.MarcTestEmployeeID will always return the same answer for that XML; ergo, it is deterministic.  Yet SQL remained unhappy.

    Happily, this question and answer about deterministic scalar functions solved the issue with a one-line change to the function definition:

    FUNCTION MarcTestEmployeeID
    (
        @SomeXml xml
    )
    RETURNS INT
    WITH SCHEMABINDING

    After creating my index, my sample “SELECT *” query returned the following, which is just what I would expect:

    image

    Next, I inserted a couple of thousand more records to make things more interesting and ran this query, which is similar to the one I plan to run in the real world:

    SELECT * FROM MarcTest WHERE TypeCode = 'CODE1' AND EmployeeID = 13791

    The execution plan indicates that my new index that’s built on the computed column using the UDF and XML data is being used:

    image

    Exactly what was hoped for!

  • Replacing an External Reference in a Database Project

    by Marc Moore | Jul 20, 2012

    Initially I thought I could just check out my project’s SOCommon.dbschema file, copy over the new version, and rebuild my database project to determine if it was compatible with the new external database’s schema; however, this did not seem to work.

    Removing the reference to the SOCommon.dbschema file and re-adding it using the new version of the file made things worse because I did not understand how to use the Add Database Reference dialog box and failed to set up the correct Database Variable.

    For some ESI projects we used SQL command variables to define the name of the SOCommon database (because it wasn’t always SOCommon in every environment).  The correct Database Reference settings for such a project are:

    SNAGHTML2305a8f3

    For other projects the references to SOCommon are made directly in the SQL objects as literals.  For such projects the correct Database Reference settings are:

    SNAGHTML23073347

    We suppress the errors caused by references in the external project because we generally don’t care about shortcomings of the SOCommon database project in regards to its referencing of the Data Warehouse and Ace Project databases.

  • Adding a New Web Service to the ESI Services Solution

    by Marc Moore | Jul 10, 2012

    Adding a new project to a Visual Studio solution is easy; however, there is a little more to getting a new ESI Services project up and running than simply creating it.  First, open Visual Studio and open the ESI Services solution. 

    To add a new project, right-click the Solution node in Solution Explorer and choose Add, New Project.  Then select WCF Rest Service Application from the dialog and name your project:

    SNAGHTML9a6309

    Visual Studio will create the default WCF Rest project in the solution:

    image

    Customizing Your Service

    Since you probably want your service to be called something other than Service1, rename this file as needed and, if prompted, answer Yes to the following dialog:

    SNAGHTML9de5de

    Alternately, you may have to rename your service’s class manually. 

    NOTE:  The ESI standard is to store the service classes in a folder named “Service”.

    Next, edit the Global.asax.cs file and "true-up” your service route registration with your new service name:

    image

    The first parameter highlighted above is the routePrefix, which will be part of the URL your service’s callers will reference; therefore, make sure it’s both human and HTTP-friendly.  As you can see, it does not have to be the same as your service’s class name.  We’ll go with a routePrefix of “Benefits” for the rest of this example.

    Next, using the default code as a guide, edit your service class so that it is only exposing the methods you want exposed.

    Consider the following:

    image

    The UriTemplate parameter to the WebGet attribute maps URL fragments and querystring parameter names to formal method names/parameters.

    Given that the Global class has a routePrefix of “Benefits”, we could reference the sample method using the URL “server/Benefits/CurrentCoverage?uin=111001111”.

    Now, as you might guess, the SampleItem DTO referenced above is created by default and you’ll obviously want to change your output to be domain-specific as well.

    NOTE:  One thing to be aware of in this process is that the EsiServices.Framework library offers a some helpful base classes and interfaces to ensure that your XML/JSON output is compatible with other ESI Services projects.  A complete discussion of this library is outside the scope of this article; see the TrainTraqRest project for a reference implementation.

    A more complete example of a service’s method might be:

    image

    Once you've got an approximation of your services’ output in place, it’s time to run your service and it it with a request in order to make sure everything is working as expected.  Do this as soon as possible in the process of starting a new project to ensure that you don’t get too far down the wrong road in the event you make a mistake.

    Testing Your Service

    It is easy enough to run your new web service on your development machine and test it out.  One little trick that helps is to ensure that your service is always running (and listening) on the same port.  To do this, right-click your project in VS’ Solution Explorer and choose Properties.  Then click the Web tab on the properties sheet.

    You may have to scroll down a bit to see the Servers section on the Web tab:

    SNAGHTMLd77ed4

    In the ESI Services project, we’ve been using ports 1234n, so browse around the solution until you find a port in that range that’s available and set your project to start the Visual Studio Development Server listening on it.

    Once this is done you can press F5 to start VS’ debugger or Control-F5 to start your project without debugging.  Either way, it will be listening on the port you specified, waiting for an HTTP request to come in that matches the route/URL format specified in your project’s service.  To test, you can point a web browser to the URL your service supports or use Fiddler.

    In this example, if Firefox is pointed to http://localhost:12348/Benefits/CurrentCoverage?uin=123001234, this is the result, which is what is expected given the code above:

    SNAGHTMLe14609

    Configuring Your Service

    It’s great to be able to compile, run, and test your service on your development machine, but eventually it needs to be deployed to the test server for integration testing, etc.  To do this, you’ll first have to create the appropriate solution and project configurations for your service project.  When this is done you can create a build definition that references your new solution configuration.

    The Debug Configuration

    First, ensure that your new project is being built in the Debug configuration.  It probably is; otherwise your testing would not have worked!

    SNAGHTML15d2320

    If you see your project, make sure it’s set to do a Debug build and that the Build checkbox is on.

    Fixing Past Mistakes

    The second thing to know about the ESI Services configurations is that by adding a new project to the solution, you have already created a problem for all of the existing projects.  That’s because VS will have added your new project to the “to build” list for all existing solution configurations, including those targeting a single ESI Services project.

    For example, consider the AuthenticationServices_DEV build below:

    SNAGHTML15b1184

    The iBenefits service should not be built during the AuthenticationServices build!!!

    If we looked at each configuration in the solution, we would see that iBenefitsRest was added to each and every one of them.  Ergo, you must remove your service from every solution configuration to which it does not apply; i.e., all of them except Debug.

    Adding New Configurations

    Adding new configurations consists of two parts: adding solution configurations and adding project configurations.  To begin with, use the Configuration Manager to add a new solution configuration:

    image

    Then name your solution configuration appropriately.

    SNAGHTML4a33d07

    NOTE:  Don’t check the “Create new…” checkbox as it will create unnecessary project configurations throughout the solution.

    Your new solution configuration will come up looking something like this:

    SNAGHTML4ae5d63

    Unfortunately, this is not close to what it should be.  Make the necessary changes to the Configuration and Build columns until it looks like this:

    SNAGHTML4affdd6

    1) Traditionally we’ve set the unbuilt projects to a Debug configuration.

    2) We’re building the Framework project so it’s always up-to-date.

    3) We next need to add a Project configuration for our web services project.

    To add a Project configuration to our solution, choose “New…” from the project Configuration dropdown:

    image

    Then name your project configuration the same as your solution configuration (unless there is a compelling reason to vary these names).

    SNAGHTML4b8885a 

    Repeat this process for the TRUNK, TRAINING, and PROD configurations.  When creating these solution configurations, you can you use the “Copy settings from…” option to copy the configuration from the DEV configuration to save a little time.

    When you’ve completed all of the configurations for your project, go to your web.config, right-click on it, and choose Add Config Transforms to create a new web.yourconfiguration.config for each project configuration you added during the steps above:

    image

    Once these files have been created, edit them to include the necessary configuration values, including connection strings, application settings, etc.

    Deploying Your Service

    Once your solution and projects configurations have been created, it’s (finally) time to create your build definition.  At this point in the solution’s life cycle, it is probably simplest to clone an existing build that’s similar to the one you want.

    NOTE:  To clone a build, you must have the TFS Power Tools installed.

    To clone a build, go to Team Explorer in Visual Studio, navigate into your TFS project, open the Builds node, right-click on the build you want to copy from, and choose Clone Build Definition. 

    If your source build was named “My Build”, you will see a new build definition named “Copy of My Build.”  There is no rename function for a build definition; to rename the new build you must edit it, which you would do anyway to set the project-specific properties of your new build. 

    To edit a build definition, right-click on it and choose Edit Build Definition.

    Rename your build on the General tab:

    image

    Check your build schedule on the Trigger tab:

    image

    Check your source code folder on the Workspace tab:

    image

    The Process tab is where you’ll actually do the work of customizing your build.  Under 1. Required, expand the “Items to Build” element and set your Configurations to Build and Projects to Build settings per your solution/configuration:

    image

    Note that the Configuration to Build is a solution configuration, not a project configuration, but if you named them the same, it doesn’t matter.

    Under 4. Esi Arguments, set the Prior build dependency field only if you are creating a build definition for a projection release.

    Under 5. Esi Database Arguments, ensure that the Execute the Deploy target on the DB Project setting is False.

    Under 6. Esi Deployment and Configuration, set the Path to startup application value to the name of your web services project:

    image

    Under 8. Esi Web Arguments, set the Target App Location to the UNC path where your new project will be deployed by this particular build definition and set the Web Deployment Configuration to the name of the project configuration to build (again, if the solution and project configurations are named the same, that is a good thing):

    image

    Save the build definition and repeat for each deployment type (Development, Trunk, Training, Production Candidate, and Production Release).

    Conclusion

    Adding a new project to the ESI Services solution is not like rolling out of bed in the morning, but hopefully the idea is less intimidating now that the process has been explained.