Skip to content

6. 3-tier architectures

6.1. Introduction

Let's revisit the latest version of the tax calculation application:


using System;

namespace Chap3 {
    class Program {
        static void Main() {
            // interactive tax calculation program
            // the user enters three pieces of data via the keyboard: married, number of children, salary
            // the program then displays the tax due
...

            // Create an IImpot object
            IImpot tax = null;
            try {
                // Create an IImpot object
                impot = new FileImpot("InvalidDataImport.txt");
            } catch (FileImpotException e) {
                // display error
...
                // terminate program
                Environment.Exit(1);
            }
            // infinite loop
            while (true) {
                // Request tax calculation parameters
                Console.Write("Tax calculation parameters in the following format: Married (y/n) No. of children Salary or 'none' to exit:");
                string parameters = Console.ReadLine().Trim();
...
                // parameters are correct - calculate the tax
                Console.WriteLine("Tax=" + tax.calculate(married == "o", numChildren, salary) + " euros");
                // next taxpayer
            }//while
        }
    }
}

The previous solution includes standard programming tasks:

  1. retrieving data stored in files, databases, etc. (lines 12–21)
  2. user interaction, lines 26 (input) and 29 (display)
  3. the use of a business algorithm, line 29

Practice has shown that isolating these different processes into separate classes improves application maintainability. The architecture of an application structured in this way is as follows:

This architecture is called a "three-tier architecture." The term "three-tier" normally refers to an architecture where each tier is on a different machine. When the tiers are on the same machine, the architecture becomes a "three-layer" architecture.

  • The [business] layer contains the application’s business rules. For our tax calculation application, these are the rules used to calculate a taxpayer’s tax. This layer requires data to function:
  • tax brackets, which change every year
  • the number of children, the taxpayer’s marital status, and annual income

In the diagram above, the data can come from two sources:

  • the data access layer or [DAO] (DAO = Data Access Object) for data already stored in files or databases. This could be the case here for tax brackets, as was done in the previous version of the application.
  • the user interface layer or [UI] (UI = User Interface) for data entered by the user or displayed to the user. This could be the case here for the number of children, marital status, and the taxpayer’s annual income
  • Generally speaking, the [DAO] layer handles access to persistent data (files, databases) or non-persistent data (network, sensors, etc.).
  • The [UI] layer, on the other hand, handles interactions with the user, if there is one.
  • The three layers are made independent through the use of interfaces.

We will revisit the [Taxes] application, which we have already studied several times, to give it a three-tier architecture. To do this, we will examine the [UI, Business Logic, DAO] layers one by one, starting with the [DAO] layer, which handles persistent data.

First, we need to define the interfaces for the different layers of the [Taxes] application.

6.2. The interfaces of the [Taxes] application

Remember that an interface defines a set of method signatures. The classes that implement the interface provide the actual implementation for these methods.

Let’s return to the 3-layer architecture of our application:

In this type of architecture, it is often the user who takes the initiative. They make a request at [1] and receive a response at [8]. This is called the request-response cycle. Let’s take the example of calculating a taxpayer’s tax. This will require several steps:

  1. The [ui] layer will need to ask the user for the number of children, marital status, and annual salary. This is operation [1] above.
  2. Once this is done, the [UI] layer will ask the business layer to calculate the tax. To do this, it will send the data it received from the user to the business layer. This is operation [2].
  3. The [business] layer needs certain information to perform its task: the tax brackets. It will request this information from the [DAO] layer using the path [3, 4, 5, 6]. [3] is the initial request and [6] is the response to that request.
  4. Having all the data it needed, the [business] layer calculates the tax.
  5. The [business] layer can now respond to the request from the [UI] layer made in (b). This is path [7].
  6. The [ui] layer will format these results and then present them to the user. This is path [8].
  7. One could imagine that the user performs tax simulations and wants to save them. They will use path [1-8] to do so.

We can see from this description that a layer uses the resources of the layer to its right, never those of the layer to its left. Consider two contiguous layers:

Layer [A] makes requests to layer [B]. In the simplest cases, a layer is implemented by a single class. An application evolves over time. Thus, layer [B] may have different implementation classes [B1, B2, ...]. If layer [B] is the [DAO] layer, it may have an initial implementation [B1] that retrieves data from a file. A few years later, we may want to store the data in a database. We will then build a second implementation class [B2]. If, in the initial application, layer [A] worked directly with class [B1], we are forced to partially rewrite the code for layer [A]. Suppose, for example, that we wrote something like the following in layer [A]:

1
2
3
B1 b1 = new B1(...);
..
b1.getData(...);
  • line 1: an instance of class [B1] is created
  • line 3: data is requested from this instance

If we assume that the new implementation class [B2] uses methods with the same signature as those of class [B1], we will have to change all [B1] references to [B2]. This is a very favorable scenario and quite unlikely if we haven’t paid attention to these method signatures. In practice, it is common for classes [B1] and [B2] to have different method signatures, meaning that a significant portion of layer [A] must be completely rewritten.

We can improve things by inserting an interface between layers [A] and [B]. This means that we define in an interface the method signatures presented by layer [B] to layer [A]. The previous diagram then becomes the following:

Layer [A] no longer communicates directly with layer [B] but with its interface [IB]. Thus, in the code for layer [A], the implementation class [Bi] of layer [B] appears only once, at the time of implementing the interface [IB]. Once this is done, it is the interface [IB] and not its implementation class that is used in the code. The previous code becomes the following:

1
2
3
IB ib = new B1(...);
..
ib.getData(...);
  • Line 1: An [ib] instance implementing the [IB] interface is created by instantiating the [B1] class
  • line 3: data is requested from the [ib] instance

Now, if we replace the [B1] implementation of layer [B] with a [B2] implementation, and both implementations adhere to the same [IB] interface, then only line 1 of layer [A] needs to be modified, and nothing else. This is a major advantage that alone justifies the systematic use of interfaces between two layers.

We can go even further and make layer [A] completely independent of layer [B]. In the code above, line 1 poses a problem because it hard-codes a reference to class [B1]. Ideally, layer [A] should be able to use an implementation of interface [IB] without having to specify a class name. This would be consistent with our diagram above. We can see that layer [A] interacts with interface [IB], and there is no reason why it would need to know the name of the class that implements this interface. This detail is not useful to layer [A].

The Spring framework (http://www.springframework.org) allows us to achieve this result. The previous architecture evolves as follows:

The cross-cutting layer [Spring] will allow a layer to obtain, via configuration, a reference to the layer to its right without needing to know the name of the class implementing that layer. This name will be in the configuration files and not in the C# code. The C# code for layer [A] then takes the following form:

1
2
3
IB ib; // initialized by Spring
..
ib.getData(...);
  • line 1: an instance [ib] implementing the [IB] interface of layer [B]. This instance is created by Spring based on information found in a configuration file. Spring will handle creating:
    • the instance [b] implementing layer [B]
    • the [a] instance implementing layer [A]. This instance will be initialized. The [ib] field above will be assigned the [b] reference of the object implementing layer [B]
  • line 3: data is requested from the [ib] instance

We can now see that the implementation class [B1] of layer B does not appear anywhere in the code of layer [A]. When the implementation [B1] is replaced by a new implementation [B2], nothing will change in the code of class [A]. We will simply change the Spring configuration files to instantiate [B2] instead of [B1].

The combination of Spring and C# interfaces brings a decisive improvement to application maintenance by making the layers of the application mutually independent. This is the solution we will use for a new version of the [Impots] application.

Let’s return to the three-tier architecture of our application:

In simple cases, we can start from the [business] layer to discover the application’s interfaces. To function, it needs data:

  • already available in files, databases, or via the network. This data is provided by the [DAO] layer.
  • not yet available. It is then provided by the [ui] layer, which obtains it from the application user.

What interface should the [DAO] layer provide to the [business] layer? What interactions are possible between these two layers? The [DAO] layer must provide the following data to the [business] layer:

  • tax brackets

In our application, the [DAO] layer uses existing data but does not create new data. A definition of the [DAO] layer interface could be as follows:


using Entities;

namespace Dao {
    public interface IImpotDao {
        // tax brackets
        TaxBracket[] TaxBrackets{get;}
    }
}
  • line 3: the [dao] layer will be placed in the [Dao] namespace
  • line 6: the IImpotDao interface defines the TranchesImpot property, which will provide the tax brackets to the [business] layer.
  • line 1: imports the namespace in which the TaxBracket structure is defined:

namespace Entities {
    // a tax bracket
    public struct TaxBracket {
        public decimal Limit { get; set; }
        public decimal CoeffR { get; set; }
        public decimal CoeffN { get; set; }
    }
}

Let’s return to the three-tier architecture of our application:

What interface should the [business] layer present to the [ui] layer? Let’s review the interactions between these two layers:

  1. The [ui] layer asks the user for the number of children, marital status, and annual salary. This is operation [1] above.
  2. Once this is done, the [ui] layer will ask the business layer to calculate the number of seats. To do this, it will pass on the data it received from the user. This is operation [2].

A definition of the [business] layer interface could be as follows:


namespace Business {
    interface IImpotMetier {
        int CalculateTax(bool married, int numChildren, int salary);
    }
}
  • Line 1: We will place everything related to the [business] layer in the [Business] namespace.
  • Line 2: The IImpotMetier interface defines only one method: the one that calculates a taxpayer’s tax based on their marital status, number of children, and annual salary.

We are examining an initial implementation of this layered architecture.

6.3. Sample Application - Version 4

6.3.1. The Visual Studio project

The Visual Studio project will be as follows:

  • [1]: The [Entities] folder contains objects that span the [UI, Business, DAO] layers: the TrancheImpot structure and the FileImpotException exception.
  • [2]: The [Dao] folder contains the classes and interfaces of the [dao] layer. We will use two implementations of the IImpotDao interface: the HardwiredImpot class discussed in Section 4.10 and FileImpot discussed in Section 5.8.
  • [3]: The [Metier] folder contains the classes and interfaces of the [business] layer
  • [4]: The [Ui] folder contains the classes of the [ui] layer
  • [5]: The [DataImpot.txt] file contains the tax brackets used by the FileImpot implementation of the [dao] layer. It is configured [6] to be automatically copied to the project’s runtime folder.

6.3.2. The application's entities

Let’s revisit the 3-tier architecture of our application:

We refer to classes that span multiple layers as entities. This generally applies to classes and structures that encapsulate data from the [dao] layer. These entities typically extend all the way up to the [ui] layer.

The application entities are as follows:

The TrancheImpot structure


namespace Entities {
    // a tax bracket
    public struct TaxBracket {
        public decimal Limit { get; set; }
        public decimal CoeffR { get; set; }
        public decimal CoeffN { get; set; }
    }
}

The FileImportException exception


using System;

namespace Entities {
    public class FileImportException : Exception {
        // error codes
        [Flags]
        public enum ErrorCodes { Access = 1, Line = 2, Field1 = 4, Field2 = 8, Field3 = 16 };

        // error code
        public ErrorCode Code { get; set; }

        // constructors
        public FileImportException() {
        }
        public FileImportException(string message)
            : base(message) {
        }
        public FileImportException(string message, Exception e)
            : base(message, e) {
        }
    }
}

Note: The FileImportException class is only useful if the [dao] layer is implemented by the FileImport class.

6.3.3. The [DAO] layer

Let’s review the [dao] layer interface:


using Entities;

namespace Dao {
    public interface IImpotDao {
        // tax brackets
        TaxBracket[] TaxBrackets{get;}
    }
}

We will implement this interface in two different ways.

First, using the HardwiredImpot class discussed in Section 4.10:


using System;
using Entities;

namespace Dao {
    public class HardwiredImpot : IImpotDao {

        // data arrays needed to calculate the tax
        decimal[] limits = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
        decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
        decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };
        // tax brackets
        public TaxBrackets[] TaxBrackets { get; private set; }

        // constructor
        public HardwiredImpot() {
            // create the tax bracket array
            TaxBrackets = new TaxBracket[limits.Length];
            // filling
            for (int i = 0; i < TaxBrackets.Length; i++) {
                TaxBrackets[i] = new TaxBracket { Limit = limits[i], RateR = rateR[i], RateN = rateN[i] };
            }
        }
    }// class
}// namespace
  • line 5: the HardwiredImpot class implements the IImpotDao interface
  • line 12: implementation of the TranchesImpot property of the IImpotDao interface. This property is an automatic property. It implements the get method of the TranchesImpot property of the IImpotDao interface. We have also declared a private set method—internal to the class—so that the constructor in lines 15–22 can initialize the tax bracket array.

The IImpotDao interface will also be implemented by the FileImpot class discussed in Section 5.8:


using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using Entities;

namespace Dao {
    class FileImport : IImpotDao {

        // data file
        public string FileName { get; set; }

        // tax brackets
        public TaxBrackets[] TaxBrackets { get; private set; }

        // constructor
        public TaxFile(string fileName) {
            // store the file name
            FileName = fileName;
            // data
            List<TaxBracket> taxBracketList = new List<TaxBracket>();
            int lineNumber = 1;
            // exception
            FileImpotException fe = null;
            // read the contents of the file fileName, line by line
            Regex pattern = new Regex(@"s*:\s*");
            // initially no error
            FileImportException.ErrorCode code = 0;
            try {
                using (StreamReader input = new StreamReader(FileName)) {
                    while (!input.EndOfStream && code == 0) {
                        // current line
                        string line = input.ReadLine().Trim();
                        // Ignore empty lines
                        if (line == "")
                            continue;
                        // line split into three fields separated by:
                        string[] lineFields = pattern.Split(line);
                        // Are there 3 fields?
                        if (lineFields.Length != 3) {
                            code = FileImportException.ErrorCodes.Line;
                        }
                        // conversions of the 3 fields
                        decimal limit = 0, coeffR = 0, coeffN = 0;
                        if (code == 0) {
                            if (!Decimal.TryParse(lineFields[0], out limit))
                                code = FileImportException.ErrorCodes.Field1;
                            if (!Decimal.TryParse(lineFields[1], out coeffR))
                                code |= FileImportException.ErrorCodes.Field2;
                            if (!Decimal.TryParse(lineFields[2], out coeffN))
                                code |= FileImpotException.ErrorCodes.Field3;
                            ;
                        }
                        // error?
                        if (code != 0) {
                            // log the error
                            fe = new FileImportException(String.Format("Invalid line {0}", lineNumber)) { Code = code };
                        } else {
                            // store the new tax bracket
                            listTaxBrackets.Add(new TaxBracket() { Limit = limit, RateR = rateR, RateN = rateN });
                            // next line
                            rowNumber++;
                        }
                    }
                }
            } catch (Exception e) {
                // log the error
                fe = new FileImportException(String.Format("Error reading file {0}", FileName), e) { Code = FileImportException.ErrorCodes.Access };
            }
            // Should an error be reported?
            if (fe != null) {
                // throw the exception
                throw fe;
            } else {
                // Return the listImpot list to the tranchesImpot array
                TranchesImpot = listTranchesImpot.ToArray();
            }
        }
    }
}
  • This code was already discussed in Section 5.8.
  • line 14: the TranchesImpot method of the IImpotDao interface
  • line 76: initialization of tax brackets in the class constructor, based on the file whose name was passed to the constructor on line 17.

6.3.4. The [business] layer

Let’s review the interface for this layer:


namespace Business {
    public interface IImpotMetier {
        int CalculateTax(bool married, int numChildren, int salary);
    }
}

The ImpotMetier implementation of this interface is as follows:


using Entities;
using Dao;

namespace Business {
    public class ImpotMetier : IImpotMetier {

        // [dao] layer
        private IImpotDao Dao { get; set; }

        // tax brackets
        private TaxBracket[] taxBrackets;

        // constructor
        public BusinessTax(IImpotDao dao) {
            // storage
            Dao = dao;
            // tax brackets
            taxBrackets = dao.TaxBrackets;
        }

        // calculate tax
        public int CalculateTax(bool married, int numChildren, int salary) {
            // calculate the number of shares
            decimal numberOfBrackets;
            if (married)
                nbParts = (decimal)nbChildren / 2 + 2;
            else
                nbParts = (decimal)nbChildren / 2 + 1;
            if (nbChildren >= 3)
                nbParts += 0.5M;
            // calculate taxable income & family quotient
            decimal income = 0.72M * salary;
            decimal QF = income / nbParts;
            // calculate tax
            taxBrackets[taxBrackets.Length - 1].Limit = QF + 1;
            int i = 0;
            while (QF > taxBrackets[i].Limit)
                i++;
            // return result
            return (int)(income * taxBrackets[i].CoeffR - numParts * taxBrackets[i].CoeffN);
        }//calculate
    }//class

}
  • line 5: the [Business] class implements the [IImpotBusiness] interface.
  • lines 14–19: the [business] layer must collaborate with the [DAO] layer. It must therefore have a reference to the object implementing the IImpotDao interface. This is why this reference is passed as a parameter to the constructor.
  • line 16: the reference to the [dao] layer is stored in the private field on line 8
  • Line 18: Using this reference, the constructor retrieves the tax bracket table and stores a reference to it in the private property on line 8.
  • Lines 22–41: Implementation of the CalculateTax method of the IImpotMetier interface. This implementation uses the tax bracket array initialized by the constructor.

6.3.5. The [ui] layer

The user interface classes in versions 2 and 3 were very similar. The one in version 2 was as follows:


using System;

namespace Chap2 {
    public class Program {
        static void Main() {
...

            // Create an IImpot object
            IImpot impot = new HardwiredImpot();

            // infinite loop
            while (true) {
...
            }//while
        }
    }
}

and that of version 3:


using System;

namespace Chap3 {
    public class Program {
        static void Main() {
...

            // Create an IImpot object
            IImpot impot = null;
            try {
                // Create an IImpot object
                impot = new FileImpot("InvalidDataImport.txt");
            } catch (FileImpotException e) {
                // display error
                string msg = e.InnerException == null ? null : String.Format(", Original exception: {0}", e.InnerException.Message);
                Console.WriteLine("The following error occurred: [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // terminate program
                Environment.Exit(1);
            }
            // infinite loop
            while (true) {
...
            }//while
        }
    }
}

The only difference is how the IImpot object, which calculates the tax, is instantiated. This object corresponds here to our [business] layer.

For a [DAO] implementation using the HardwiredImpot class, the dialog class is as follows:


using System;
using Metier;
using Dao;
using Entities;

namespace Ui {
    public class Dialogue2 {
        static void Main() {
...

            // we create the [business and DAO] layers
            IImpotMetier business = new ImpotMetier(new HardwiredImpot());

            // infinite loop
            while (true) {
...
                // the parameters are correct - we calculate the tax
                Console.WriteLine("Tax=" + job.CalculateTax(married == "o", numberOfChildren, salary) + " euros");
                // next taxpayer
            }//while
        }
    }
}
  • line 12: instantiation of the [dao] and [business] layers. Note that the [business] layer requires the [dao] layer.
  • line 18: use of the [business] layer to calculate the tax

For a [dao] implementation using the FileImpot class, the dialog class is as follows:


using System;
using Metier;
using Dao;
using Entities;

namespace Ui {
    public class Dialog {
        static void Main() {
...
            // Create the [business and DAO] layers
            IImpotMetier business = null;
            try {
        // create [business] layer
                business = new BusinessImport(new FileImport("DataImport.txt"));
            } catch (FileImportException e) {
                // display error
                string msg = e.InnerException == null ? null : String.Format(", Original exception: {0}", e.InnerException.Message);
                Console.WriteLine("The following error occurred: [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // terminate program
                Environment.Exit(1);
            }
            // infinite loop
            while (true) {
...
                // the parameters are correct - we calculate the tax
                Console.WriteLine("Tax=" + job.CalculateTax(married == "o", numberOfChildren, salary) + " euros");
                // next taxpayer
            }//while
        }
    }
}
  • lines 11–21: instantiation of the [dao] and [business] layers. Since instantiating the [dao] layer may throw an exception, this is handled
  • line 26: use of the [business] layer to calculate the tax, as in the previous version

6.3.6. Conclusion

The layered architecture and the use of interfaces have brought a certain flexibility to our application. This is particularly evident in the way the [ui] layer instantiates the [dao] and [business] layers:


        // Create the [business logic and DAO] layers
IImpotMetier metier = new ImpotMetier(new HardwiredImpot());

in one case:


// we create the layers [business and DAO]
            IImpotMetier business = null;
            try {
        // create [business] layer
                business = new BusinessImport(new FileImport("DataImport.txt"));
            } catch (FileImportException e) {
                // display error
                string msg = e.InnerException == null ? null : String.Format(", Original exception: {0}", e.InnerException.Message);
                Console.WriteLine("The following error occurred: [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // terminate program
                Environment.Exit(1);
            }

in the other. Except for exception handling in case 2, the instantiation of the [dao] and [business] layers is similar in both applications. Once the [dao] and [business] layers are instantiated, the code in the [ui] layer is identical in both cases. This is because the [business] layer is manipulated via its IImpotMetier interface and not via its implementation class. Changing the [business] or [DAO] layer of the application without changing their interfaces will always amount to changing only the preceding lines in the [UI] layer.

Another example of the flexibility provided by this architecture is the implementation of the [business] layer:


using Entities;
using Dao;

namespace Business {
    public class BusinessTax : IImpotMetier {

        // [DAO] layer
        private IImpotDao Dao { get; set; }

        // tax brackets
        private TaxBrackets[] taxBrackets;

        // constructor
        public BusinessTax(IImpotDao dao) {
            // storage
            Dao = dao;
            // tax brackets
            taxBrackets = dao.TaxBrackets;
        }

        // tax calculation
        public int CalculateTax(bool married, int numChildren, int salary) {
...
        }//calculate
    }//class

}

In line 14, we see that the [business] layer is built using a reference to the [DAO] layer interface. Changing the implementation of the latter therefore has zero impact on the [business] layer. This is why our single implementation of the [business] layer was able to work without modifications with two different implementations of the [DAO] layer.

6.4. Example Application - version 5

This new version builds on the previous one with the following changes:

  • The [business] and [DAO] layers are each encapsulated in a DLL and tested using the NUnit unit testing framework.
  • Layer integration is handled by the Spring framework

In large projects, multiple developers work on the same project. Layered architectures facilitate this way of working: because the layers communicate with each other via well-defined interfaces, a developer working on one layer does not have to worry about the work of other developers on the other layers. Everyone simply needs to adhere to the interfaces.

In the example above, when testing the [business] layer, the developer will need an implementation of the [DAO] layer. Until that is finished, they can use a mock implementation of the [DAO] layer as long as it adheres to the IImpotDao interface. This is another advantage of layered architecture: a delay in the [DAO] layer does not prevent testing of the [business] layer. The mock implementation of the [DAO] layer also has the advantage of often being much easier to set up than the actual [DAO] layer, which may require launching a DBMS, establishing network connections, etc.

Once the [DAO] layer is complete and tested, it will be provided to the [business] layer developers in the form of a DLL rather than source code. Ultimately, the application is often delivered as an .exe executable (for the [UI] layer) and .dll class libraries (for the other layers).

6.4.1. NUnit

The tests performed so far for our various applications relied on visual verification. We would verify that what was expected appeared on the screen. This method is impractical when there are many tests to run. Humans are prone to fatigue, and their ability to verify tests diminishes over the course of the day. Tests must therefore be automated and designed to require no human intervention.

An application evolves over time. With each update, we must verify that the application does not “regress,” i.e., that it continues to pass the functional tests that were performed during its initial development. These tests are called “non-regression” tests. A moderately large application may require hundreds of tests. In fact, every method in every class of the application is tested. These are called unit tests. They can tie up a lot of developers if they haven’t been automated.

Tools have been developed to automate testing. One of them is called NUnit. It is available at [http://www.nunit.org]:

Version 2.4.6 shown above was used for this document (March 2008). The installation places an icon [1] on the desktop:

Double-clicking the icon [1] launches the NUnit graphical user interface [2]. This does nothing to help with test automation, since we are once again reduced to a visual check: the tester verifies the test results displayed in the graphical user interface. Nevertheless, tests can also be run using batch tools, and their results saved to XML files. This is the method used by development teams: tests are run overnight, and developers have the results the next morning.

Let’s examine the principle of NUnit testing with an example. First, let’s create a new C# project of the Console Application type:

In [1], we see the project references. These references are DLLs containing classes and interfaces used by the project. Those shown in [1] are included by default in every new C# project. To use the classes and interfaces of the NUnit framework, we need to add [2] a new reference to the project.

In the .NET tab above, we select the [nunit.framework] component. The [nunit.*] components listed above are not components present by default in the .NET environment. They were added there by the previous installation of the NUnit framework. Once the reference has been added, it appears [4] in the project’s reference list.

Before building the application, the project's [bin/Release] folder is empty. After building (F6), you can see that the [bin/Release] folder is no longer empty:

In [6], we can see the presence of the DLL [nunit.framework.dll]. It was the addition of the [nunit.framework] reference that caused this DLL to be copied into the runtime folder. This is one of the folders that will be searched by the .NET CLR (Common Language Runtime) to find the classes and interfaces referenced by the project.

Let’s create our first NUnit test class. To do this, we’ll delete the default [Program.cs] class and then add a new class [Nunit1.cs] to the project. We’ll also remove the unnecessary references [7].

The NUnit1 test class will be as follows:


using System;
using NUnit.Framework;

namespace NUnit {
    [TestFixture]
    public class NUnit1 {
        public NUnit1() {
            Console.WriteLine("constructor");
        }
        [SetUp]
        public void before() {
            Console.WriteLine("Setup");
        }
        [TearDown]
        public void after() {
            Console.WriteLine("TearDown");
        }
        [Test]
        public void t1() {
            Console.WriteLine("test1");
            Assert.AreEqual(1, 1);
        }
        [Test]
        public void t2() {
            Console.WriteLine("test2");
            Assert.AreEqual(1, 2, "1 is not equal to 2");
        }
    }
}
  • Line 6: The NUnit1 class must be public. The public keyword is not generated by default in Visual Studio. You must add it.
  • Line 5: The [TestFixture] attribute is a NUnit attribute. It indicates that the class is a test class.
  • Lines 7–9: The constructor. It is used here only to display a message on the screen. We want to see when it is executed.
  • Line 10: The [SetUp] attribute defines a method executed before each unit test.
  • Line 14: The [TearDown] attribute defines a method executed after each unit test.
  • Line 18: The [Test] attribute defines a test method. For each method annotated with the [Test] attribute, the method annotated with [SetUp] will be executed before the test, and the method annotated with [TearDown] will be executed after the test.
  • Line 21: One of the [Assert.*] methods defined by the NUnit framework. The following [Assert] methods are available:
    • [Assert.AreEqual(expression1, expression2)]: checks that the values of the two expressions are equal. Many expression types are accepted (int, string, float, double, decimal, ...). If the two expressions are not equal, an exception is thrown.
    • [Assert.AreEqual(real1, real2, delta)]: checks that two real numbers are equal to within delta, i.e., abs(real1 - real2) <= delta. For example, you can write [Assert.AreEqual(real1, real2, 1E-6)] to verify that two values are equal to within 10⁻⁶.
    • [Assert.AreEqual(expression1, expression2, message)] and [Assert.AreEqual(real1, real2, delta, message)] are variants that allow you to specify the error message to be associated with the exception thrown when the [Assert.AreEqual] method fails.
    • [Assert.IsNotNull(object)] and [Assert.IsNotNull(object, message)]: checks that object is not equal to null.
    • [Assert.IsNull(object)] and [Assert.IsNull(object, message)]: checks that object is equal to null.
    • [Assert.IsTrue(expression)] and [Assert.IsTrue(expression, message)]: checks that expression is equal to true.
    • [Assert.IsFalse(expression)] and [Assert.IsFalse(expression, message)]: checks that expression is equal to false.
    • [Assert.AreSame(object1, object2)] and [Assert.AreSame(object1, object2, message)]: checks that the references object1 and object2 point to the same object.
    • [Assert.AreNotSame(object1, object2)] and [Assert.AreNotSame(object1, object2, message)]: checks that the references object1 and object2 do not point to the same object.
  • line 21: the assertion must pass
  • line 26: the assertion must fail

Let’s configure the project so that it generates a DLL instead of an .exe executable:

  • in [1]: project properties
  • in [2, 3]: for the project type, select [Class Library]
  • in [4]: the project build will produce a DLL (assembly) named [Nunit.dll]

Now let’s use NUnit to run the test class:

  • in [1]: open a NUnit project
  • in [2, 3]: load the DLL bin/Release/Nunit.dll generated by the C# project
  • in [4]: the DLL has been loaded
  • in [5]: the test tree
  • in [6]: running the tests
  • in [7]: the results: t1 passed, t2 failed
  • in [8]: a red bar indicates that the test suite has failed
  • in [9]: the error message related to the failed test
  • in [11]: the various tabs in the results window
  • in [12]: the [Console.Out] tab. Here we can see that:
    • the constructor was executed only once
    • the [SetUp] method was executed before each of the two tests
    • the [TearDown] method was executed after each of the two tests

You can specify which methods to test:

  • in [1]: we request that a checkbox be displayed next to each test
  • in [2]: check the tests to be executed
  • in [3]: run them

To fix errors, simply correct the C# project and rebuild it. NUnit detects that the DLL it is testing has been changed and automatically loads the new one. You then just need to rerun the tests.

Consider the following new test class:


using System;
using NUnit.Framework;

namespace NUnit {
    [TestFixture]
    public class NUnit2 : AssertionHelper {
        public NUnit2() {
            Console.WriteLine("constructor");
        }
        [SetUp]
        public void before() {
            Console.WriteLine("Setup");
        }
        [TearDown]
        public void after() {
            Console.WriteLine("TearDown");
        }
        [Test]
        public void t1() {
            Console.WriteLine("test1");
            Expect(1, EqualTo(1));
        }
        [Test]
        public void t2() {
            Console.WriteLine("test2");
            Expect(1, EqualTo(2), "1 is not equal to 2");
        }
    }
}

Starting with version 2.4 of NUnit, a new syntax became available, as seen in lines 21 and 26. To use this, the test class must derive from the AssertionHelper class (line 6).

The (non-exhaustive) mapping between the old and new syntax is as follows:

Assert.AreEqual(expression1, expression2,
message)
Expect(expression1, EqualTo(expression2), message)
Assert.AreEqual(real1, real2, delta, message)
Expect(expression1, EqualTo(expression2).Within(delta),
message)
Assert.AreSame(object1, object2, message)
Expect(object1, SameAs(object2), message)
Assert.AreNotSame(object1, object2, message)
Expect(object1, Not.SameAs(object2), message)
Assert.IsNull(object, message)
Expect(object, Null, message)
Assert.IsNotNull(object, message)
Expect(object, Not.Null, message)
Assert.IsTrue(expression, message)
Expect(expression, True, message)
Assert.IsFalse(expression, message)
Expect(expression,False,message)

Let's add the following test to the NUnit2 class:


        [Test]
        public void t3() {
            bool true = true, false = false;
            Expect(true, True);
            Expect(false, False);
            Object obj1 = new Object(), obj2 = null, obj3 = obj1;
            Expect(obj1, Not.Null);
            Expect(obj2, Null);
            Expect(obj3, SameAs(obj1));
            double d1 = 4.1, d2 = 6.4, d3 = d1;
            Expect(d1, EqualTo(d3).Within(1e-6));
            Expect(d1, Not.EqualTo(d2));
}

If we build (F6) the new DLL for the C# project, the NUnit project becomes the following:

  • in [1]: the new [NUnit2] test class was automatically detected
  • in [2]: we run the NUnit2 test t3
  • in [3]: test t3 passed

To learn more about NUnit, refer to the NUnit help:

6.4.2. The Visual Studio Solution

We will gradually build the following Visual Studio solution:

  • in [1]: the ImpotsV5 solution consists of three projects, one for each of the application’s three layers
  • in [2]: the [dao] project of the [dao] layer
  • in [3]: the [business] project of the [business] layer
  • in [4]: the [ui] project of the [ui] layer

The ImpotsV5 solution can be built as follows:

1
234
5
  • in [1]: create a new project
  • in [2]: choose a console application
  • in [3]: call the [dao] project
  • in [4]: create the project
  • in [5]: once the project is created, save it
  • in [6]: keep the name [dao] for the project
  • in [7]: specify a folder to save the project and its solution
  • in [8]: name the solution
  • in [9]: specify that the solution should have its own folder
  • in [10]: save the project and its solution
  • in [11]: the [dao] project within its ImpotsV5 solution
  • in [12]: the ImpotsV5 solution folder. It contains the [dao] folder from the [dao] folder.
  • in [13]: the contents of the [dao] folder
  • in [14]: add a new project to the ImpotsV5 solution
  • in [15]: the new project is named [metier]
  • in [16]: the solution with its two projects
  • in [17]: the solution, after the third project [ui] has been added
  • in [18]: the solution folder and the folders for the three projects
  • When you run a solution (Ctrl+F5), the active project is executed. The same applies when you build (F6) the solution. The name of the active project is bolded [19] in the solution.
  • in [20]: to change the active project in the solution
  • in [21]: the [business] project is now the active project in the solution

6.4.3. The [ DAO] layer

Project references (see [1] in the project)

We add the [nunit.framework] reference required for [NUnit] tests

The entities (see [2] in the project)

The [TrancheImpot] class is the one from previous versions. The [FileImpotException] class from the previous version is renamed to [ImpotException] to make it more generic and not tie it to a specific [DAO] layer:


using System;

namespace Entities {
    public class ImpotException : Exception {

        // error code
        public int Code { get; set; }

        // constructors
        public ImpotException() {
        }
        public ImpotException(string message)
            : base(message) {
        }
        public ImpotException(string message, Exception e)
            : base(message, e) {
        }
    }
}

The [dao] layer (see [3] in the project)

The [IImpotDao] interface is the same as in the previous version. The same applies to the [HardwiredImpot] class. The [FileImpot] class has been updated to reflect the change from the [FileImpotException] to [ImpotException]:


...

namespace Dao {
    public class FileImpot : IImpotDao {

        // error codes
        [Flags]
        public enum ErrorCodes { Access = 1, Line = 2, Field1 = 4, Field2 = 8, Field3 = 16 };

...

        // constructor
        public FileImport(string fileName) {
            // store the file name
            FileName = fileName;
...
            // initially no errors
            ErrorCode code = 0;
            try {
                using (StreamReader input = new StreamReader(FileName)) {
                    while (!input.EndOfStream && code == 0) {
...
                        // error?
                        if (code != 0) {
                            // Log the error
                            fe = new ImpotException(String.Format("Invalid line {0}", lineNumber)) { Code = (int)code };
                        } else {
...
                        }
                    }
                }
            } catch (Exception e) {
                // log the error
                fe = new ImpotException(String.Format("Error reading file {0}", FileName), e) { Code = (int)ErrorCodes.Access };
            }
            // Should the error be reported?
...
        }
    }
}
  • line 8: the error codes previously in the [FileImpotException] class have been moved to the [FileImpot] class. These are, in fact, error codes specific to this implementation of the [IImpotDao] interface.
  • lines 26 and 34: to encapsulate an error, the [ImpotException] class is used instead of the [FileImpotException] class.

The [Test1] test (see [4] in the project)

The [Test1] class simply displays the tax brackets on the screen:


using System;
using Dao;
using Entities;

namespace Tests {
    class Test1 {
        static void Main() {

            // Create the [dao] layer
            IImpotDao dao = null;
            try {
                // Create the [DAO] layer
                dao = new FileImport("DataImport.txt");
            } catch (ImportException e) {
                // display error
                string msg = e.InnerException == null ? null : String.Format(", Original exception: {0}", e.InnerException.Message);
                Console.WriteLine("The following error occurred: [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // terminate program
                Environment.Exit(1);
            }
            // display tax brackets
            TaxBrackets[] taxBrackets = dao.TaxBrackets;
            foreach (TaxBracket t in taxBrackets) {
                Console.WriteLine("{0}:{1}:{2}", t.Limit, t.Rate, t.Amount);
            }
        }
    }
}
  • Line 13: The [dao] layer is implemented by the [FileImpot] class
  • line 14: we handle the [ImpotException] exception that may occur.

The [DataImpot.txt] file required for testing is automatically copied to the project's runtime folder (see [5] in the project). The [dao] project will have several classes containing a [Main] method. You must therefore explicitly specify the class to run when the user requests execution of the project using Ctrl-F5:

  • in [1]: access the project properties
  • in [2]: specify that it is a console application
  • in [3]: specify the class to run

Executing the previous [Test1] class yields the following results:

4962:0:0
8382:0.068:291.09
14753:0.191:1322.92
23888:0.283:2668.39
38868:0.374:4846.98
47932:0.426:6883.66
0:0.481:9505.54

The [Test2] test (see [4] in the project)

The [Test2] class does the same thing as the [Test1] class by implementing the [dao] layer with the [HardwiredImpot] class. Line 13 of [Test1] is replaced by the following:


                dao = new HardwiredImpot();

The project is modified to now run the [Test2] class:

The screen results are the same as before.

The NUnit test [NUnit1] (see [4] in the project)

The unit test [NUnit1] is as follows:


using System;
using Dao;
using Entities;
using NUnit.Framework;

namespace Tests {
    [TestFixture]
    public class NUnit1 : AssertionHelper{
        // [DAO] layer to be tested
        private IImpotDao dao;

        // constructor
        public NUnit1() {
            // Initialize [DAO] layer
            dao = new FileImpot("DataImpot.txt");
        }

        // test
        [Test]
        public void ShowTaxBrackets(){
            // display the tax brackets
            TaxBrackets[] taxBrackets = dao.TaxBrackets;
            foreach (TaxBracket t in taxBrackets) {
                Console.WriteLine("{0}:{1}:{2}", t.Limit, t.Rate, t.Amount);
            }
            // some tests
            Expect(taxBrackets.Length, EqualTo(7));
            Expect(taxBrackets[2].Limit, EqualTo(14753));
            Expect(taxBrackets[2].Rate, EqualTo(0.191));
            Expect(taxBrackets[2].CoeffN, EqualTo(1322.92));
        }
    }
}
  • The test class derives from the [AssertionHelper] class, which allows the use of the static Expect method (lines 27–30).
  • line 10: a reference to the [dao] layer
  • lines 13–16: the constructor instantiates the [dao] layer with the [FileImport] class
  • lines 19–20: the test method
  • line 22: we retrieve the array of tax brackets from the [dao] layer
  • lines 23–25: we display them as before. This display would not be necessary in a real unit test. Here, it serves an educational purpose.
  • line 27: we verify that there are indeed 7 tax brackets
  • Lines 28–30: We verify the values for tax bracket #2

To run this unit test, the project must be of type [Class Library]:

  • in [1]: the project type has been changed
  • in [2]: the generated DLL will be named [ ImpotsV5-dao.dll]
  • in [3]: after generating (F6) the project, the [dao/bin/Release] folder contains the DLL [ImpotsV5-dao.dll]

The DLL [ImpotsV5-dao.dll] is then loaded into the NUnit framework and executed:

  • in [1]: the tests were successful. We now consider the [dao] layer operational. Its DLL contains all the classes in the project, including the test classes. These are unnecessary. We rebuild the DLL to exclude the test classes.
  • in [2]: the [tests] folder is excluded from the project
  • in [3]: the new project. This is regenerated by pressing F6 to generate a new DLL.

6.4.4. The [ business] layer

  • in [1], the [business] project has become the active project in the solution
  • in [2]: the project references
  • in [3]: the [business] layer
  • in [4]: the test classes
  • in [5]: the [DataImpot.txt] file containing the tax brackets configured [6] to be automatically copied to the project's runtime folder [7]

Project references (see [2] in the project)

As with the [dao] project, we add the [nunit.framework] reference required for [NUnit] tests. The [business] layer requires the [dao] layer. It therefore needs a reference to this layer’s DLL. Proceed as follows:

  • in [1]: add a new reference to the [business] project references
  • in [2]: select the [Browse] tab
  • in [3]: select the [dao/bin/Release] folder
  • in [4]: select the [ImpotsV5-dao.dll] DLL generated in the [dao] project
  • in [5]: the new reference

The [metier] layer (see [3] in the project)

The [IImpotMetier] interface is the same as in the previous version. The same applies to the [ImpotMetier] class.

The [Test1] test (see [4] in the project)

The [Test1] class simply performs a few salary calculations:


using System;
using Dao;
using Entities;
using Metier;

namespace Tests {
    class Test1 {
        static void Main() {

            // Create the [business] layer
            IImpotMetier business = null;
            try {
                // Create the [business] layer
                business = new BusinessImport(new FileImport("DataImport.txt"));
            } catch (ImportException e) {
                // display error
                string msg = e.InnerException == null ? null : String.Format(", Original exception: {0}", e.InnerException.Message);
                Console.WriteLine("The following error occurred: [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // terminate program
                Environment.Exit(1);
            }
            // calculate some taxes
            Console.WriteLine(String.Format("Tax(true,2,60000)={0} euros", job.CalculateTax(true, 2, 60000)));
            Console.WriteLine(String.Format("Tax(false,3,60000)={0} euros", business.CalculateTax(false, 3, 60000)));
            Console.WriteLine(String.Format("Tax(false, 3, 60000) = {0} euros", business.CalculateTax(false, 3, 6000)));
            Console.WriteLine(String.Format("Tax(false, 3, 60000) = {0} euros", job.CalculateTax(false, 3, 600000)));
        }
    }
}
  • Line 14: creation of the [business] and [DAO] layers. The [DAO] layer is implemented with the [FileImpot] class
  • lines 12–21: handling of a potential [ImpotException]
  • lines 23–26: repeated calls to the single CalculerImpot method of the [IImpotMetier] interface.

The [business] project is configured as follows:

  • [1]: the project is a console application
  • [2]: The executed class is the [Test1] class
  • [3]: Project generation will produce the executable [ImpotsV5-metier.exe]

Running the project yields the following results:

1
2
3
4
Tax(true,2,60000)=4282 euros
Tax(false, 3, 60000) = 4282 euros
Tax(false,3,60000)=0 euros
Tax(false,3,60000)=179,275 euros

The [NUnit1] test (see [4] in the project)

The unit test class [NUnit1] takes the four previous calculations and verifies their results:


using Dao;
using Metier;
using NUnit.Framework;

namespace Tests {
    [TestFixture]
    public class NUnit1:AssertionHelper {
        // [business] layer to be tested
        private IImpotMetier business;

        // constructor
        public NUnit1() {
            // initialize [business] layer
            business = new BusinessImport(new FileImport("DataImport.txt"));
        }

        // test
        [Test]
        public void TaxCalculations(){
            // display tax brackets
            Expect(business.CalculateTax(true, 2, 60000), EqualTo(4282));
            Expect(business.CalculateTax(false, 3, 60000), EqualTo(4282));
            Expect(business.CalculateTax(false, 3, 6000), EqualTo(0));
            Expect(business.CalculateTax(false, 3, 600000), EqualTo(179275));
        }
    }
}
  • line 14: creation of the [business] and [dao] layers. The [dao] layer is implemented with the [FileImpot] class
  • Lines 21–24: Repeated calls to the single CalculateTax method of the [IImpotMetier] interface with result verification.

The [business] project is now configured as follows:

  • [1]: the project is of type "class library"
  • [2]: Generating the project will produce the DLL [ImpotsV5-metier.dll]

The project is generated (F6). Then the generated DLL [ ImpotsV5-metier.dll] is loaded into NUnit and tested:

 

Above, the tests were successful. We now consider the [business] layer operational. Its DLL contains all the project’s classes, including the test classes. These are unnecessary. We rebuild the DLL to exclude the test classes.

  • In [1]: the [tests] folder is excluded from the project
  • in [2]: the new project. This is regenerated by pressing F6 to generate a new DLL.

6.4.5. The [ui] layer

  • in [1], the [ui] project has become the active project in the solution
  • in [2]: the project references
  • in [3]: the [ui] layer
  • in [4]: the [DataImpot.txt] file containing tax brackets, configured [5] to be automatically copied to the project’s runtime folder [6]

Project references (see [2] in the project)

The [ui] layer requires the [business] and [dao] layers to perform its tax calculations. It therefore needs a reference to the DLLs of these two layers. Proceed as shown for the [business] layer

The main class [Dialogue.cs] (see [3] in the project)

The [Dialogue.cs] class is the same as in the previous version.

Tests

The [ui] project is configured as follows:

  • [1]: The project is a "console application"
  • [2]: Building the project will produce the executable [ImpotsV5-ui.exe]
  • [3]: the class that will be executed

An example of execution (Ctrl+F5) is as follows:

Tax calculation parameters in the following format: Married (y/n) No. of Children Salary or nothing to stop at: 2 60000
Tax = 4,282 euros

6.4.6. The [ Spring] layer

Let’s return to the code in [Dialogue.cs] that creates the [dao] and [business] layers:


// we create the [business and dao] layers
            IImpotMetier business = null;
            try {
        // create [business] layer
                business = new BusinessImport(new FileImport("DataImport.txt"));
            } catch (ImpotException e) {
                // display error
...
                // terminate program
                Environment.Exit(1);
            }

Line 5 creates the [dao] and [business] layers by explicitly naming the implementation classes for both layers: FileImpot for the [dao] layer, ImpotMetier for the [business] layer. If the implementation of one of the layers is done with a new class, line 5 will be changed. For example:


                business = new BusinessLayer(new HardwiredBusinessLayer());

Apart from this change, nothing else will change in the application because each layer communicates with the next via an interface. As long as the interface remains unchanged, communication between layers remains unchanged as well. The Spring framework allows us to take layer independence a step further by letting us externalize the names of the classes implementing the s of the different layers into a configuration file. Changing the implementation of a layer then amounts to changing a configuration file. There is no impact on the application code.

Above, the [ui] layer will ask [0] Spring to instantiate the [dao] [1] and [business] [2] layers based on the information contained in a configuration file. The [ui] layer will then ask Spring [3] for a reference to the [business] layer:


            // we create the [business] and [dao] layers
            IImpotMetier business = null;
            try {
                // Spring context
                IApplicationContext ctx = ContextRegistry.GetContext();
                // request a reference to the [business] layer
                business = (IImpotMetier)ctx.GetObject("business");
            } catch (Exception e1) {
...
}
  • Line 5: Instantiation of the [dao] and [business] layers by Spring
  • line 7: a reference to the [business] layer is retrieved. Note that the [ui] layer obtained this reference without specifying the name of the class implementing the [business] layer.

The Spring framework exists in two versions: Java and .NET. The .NET version is available at the URL (March 2008) [http://www.springframework.net/]:

  • in [1]: the [Spring.net] website
  • in [2]: the downloads page
  • at [3]: download Spring 1.1 (March 2008)
  • at [4]: download the .exe version and install it
  • at [5]: the folder generated by the installation
  • [6]: The [bin/net/2.0/release] folder contains the Spring DLLs for Visual Studio .NET 2.0 or higher projects. Spring is a feature-rich framework. The aspect of Spring we will use here to manage layer integration in an application is called IoC (Inversion of Control) or DI (Dependency Injection). Spring provides libraries for database access with NHibernate, the generation and operation of web services, web applications, and more.
  • The DLLs required to manage layer integration in an application are DLLs [7] and [8].

We store these three DLLs in a [lib] folder within our project:

  • [1]: The three DLLs are placed in the [lib] folder using Windows Explorer
  • [2]: In the [ui] project, we display all files
  • [3]: The [ui/lib] folder is now visible. We include it in the project
  • [4]: The [ui/lib] folder is part of the project

Creating the [lib] folder is by no means essential. References could be created directly to the three DLLs in the [bin/net/2.0/release] folder of [Spring.net]. However, creating the [lib] folder allows the application to be developed on a machine that does not have [Spring.net] installed, making it less dependent on the available development environment.

We add references to the three new DLLs to the [ui] project:

  • [1]: We create references to the three DLLs in the [lib] folder [2]
  • [3]: The three DLLs are now part of the project references

Let’s return to an overview of the application architecture:

Above, the [ui] layer will ask [0] Spring to instantiate the [dao] [1] and [business] [2] layers based on the information contained in a configuration file. The [ui] layer will then ask Spring [3] for a reference to the [business] layer. This will be reflected in the [ui] layer with the following code:


            // create the [business] and [dao] layers
            IImpotMetier business = null;
            try {
                // Spring context
                IApplicationContext ctx = ContextRegistry.GetContext();
                // request a reference to the [business] layer
                business = (IImpotMetier)ctx.GetObject("business");
            } catch (Exception e1) {
...
}
  • Line 5: Instantiation of the [dao] and [business] layers by Spring
  • line 7: retrieving a reference to the [business] layer.

Line [5] above uses the [App.config] configuration file from the Visual Studio project. In a C# project, this file is used to configure the application. [App.config] is therefore not a Spring concept but a Visual Studio concept that Spring utilizes. Spring can use configuration files other than [App.config]. The solution presented here is therefore not the only one available.

Let’s create the [App.config] file using the Visual Studio wizard:

  • in [1]: add a new item to the project
  • in [2]: select "Application Configuration File"
  • in [3]: [App.config] is the default name for this configuration file
  • in [4]: the [App.config] file has been added to the project

The contents of the [App.config] file are as follows:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
</configuration>

[ App.config] is an XML file. The project configuration is defined between the <configuration> tags. The configuration required by Spring is as follows:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <object name="dao" type="Dao.FileImpot, ImpotsV5-dao">
                <constructor-arg index="0" value="DataImpot.txt"/>
            </object>
            <object name="business" type="Business.BusinessTax, TaxesV5-business">
                <constructor-arg index="0" ref="dao"/>
            </object>
        </objects>
    </spring>
</configuration>
  • Lines 11–23: The section delimited by the <spring> tag is called the <spring> section group. You can create as many section groups as you want in [App.config].
  • A section group contains sections: this is the case here:
    • Lines 12–14: the <spring/context> section
    • lines 15-22: the <spring/objects> section
  • lines 4-9: the <configSections> region defines the list of handlers for the section groups present in [App.config].
  • Lines 5–8: define the list of handlers for the sections in the <spring> group (name="spring").
  • line 6: the handler for the <context> section of the <spring> group:
    • name: name of the managed section
    • type: name of the class managing the section in the format ClassName, DLLName.
    • The <context> section of the <spring> group is managed by the class [Spring.Context.Support.ContextHandler], which can be found in the [Spring.Core.dll] DLL
  • Line 7: The handler for the <objects> section of the <spring> group

Lines 4–9 are standard in an [App.config] file with Spring. We simply copy them from one project to another.

  • Lines 12–14: Define the <spring/context> section.
  • Line 13: The <resource> tag is used to specify the location of the file defining the classes that Spring must instantiate. These can be in [App.config] as shown here, but they can also be in another configuration file. The location of these classes is specified in the uri attribute of the <resource> tag:
    • <resource uri="config://spring/objects"> indicates that the list of classes to be instantiated is located in the [App.config] file (config:), within the //spring/objects section, i.e., within the <objects> tag of the <spring> tag.
    • <resource uri="file://spring-config.xml"> would indicate that the list of classes to instantiate is located in the [spring-config.xml] file. This file should be placed in the project's runtime directories (bin/Release or bin/Debug). The simplest approach is to place it, as was done for the [DataImport.txt] file, in the project root with the property [Copy to output directory=always].

Lines 12–14 are standard in an [App.config] file with Spring. We simply copy them from one project to another.

  • Lines 15–22: define the classes to be instantiated. This is where the application-specific configuration takes place. The <objects> tag delimits the section defining the classes to be instantiated.
  • Lines 16–18: define the class to be instantiated for the [DAO] layer
    • Line 16: Each object instantiated by Spring is enclosed in a <object> tag. This tag has a name attribute, which is the name of the instantiated object. It is through this attribute that the application requests a reference from Spring: "give me a reference to the object named dao". The type attribute defines the class to be instantiated in the form ClassName, DLLName. Thus, line 16 defines an object named "dao," an instance of the "Dao.FileImpot" class located in the "ImpotsV5-dao.dll" DLL. Note that we provide the full class name (including the namespace) and that the .dll suffix is not specified in the DLL name.

A class can be instantiated in two ways with Spring:

  1. via a specific constructor to which parameters are passed: this is what is done in lines 16–18.
  2. via the default constructor without parameters. The object is then initialized via its public properties: the <object> tag has <property> sub-tags to initialize these various properties. We do not have an example of this case here.
  • (continued)
    • line 16: the instantiated class is the FileImport class. It has the following constructor:

        public FileImport(string fileName);

The constructor parameters are defined using <constructor-arg> tags.

  • line 17: defines the first and only parameter of the constructor. The index attribute is the constructor parameter number, and the value attribute is its value: <constructor-arg index="i" value="valuei"/>
  • Lines 19–21: define the class to be instantiated for the [business] layer: the [Metier.ImpotMetier] class located in the [ImpotsV5-metier.dll] DLL.
    • line 19: the instantiated class is the ImpotMetier class. This class has the following constructor:

        public ImpotMetier(IImpotDao dao);
  • (continued)
    • line 20: defines the first and only parameter of the constructor. Above, the dao parameter of the constructor is an object reference. In this case, within the <constructor-arg> tag, the ref attribute is used instead of the value attribute that was used for the [dao] layer: <constructor-arg index="i" ref="refi"/>. In the constructor above, the dao parameter represents an instance on the [dao] layer. This instance was defined by lines 16–18 of the configuration file. Thus, in line 20:

                <constructor-arg index="0" ref="dao"/>

ref="dao" represents the Spring "dao" object defined in lines 16–18.

To summarize, the [App.config] file:

  • instantiates the [dao] layer with the FileImport class, which takes DataImport.txt as a parameter (lines 16–18). The resulting object is called "dao"
  • instantiates the [business] layer using the ImpotMetier class, which takes the previous "dao" object as a parameter (lines 19–21).

All that remains is to use this Spring configuration file in the [ui] layer. To do this, we duplicate the [Dialogue.cs] class as [Dialogue2.cs] and make the latter the main class of the [ui] project:

  • in [1]: copy of [Dialogue.cs]
  • in [2]: paste
  • in [3]: the copy of [Dialogue.cs]
  • in [4]: renamed to [Dialogue2.cs]
  • in [6]: we make [Dialogue2.cs] the main class of the [ui] project.

The following code from [Dialogue.cs]:


            // create the [business and DAO] layers
            IImpotMetier business = null;
            try {
        // create the [business] layer
                business = new BusinessImport(new FileImport("DataImport.txt"));
            } catch (ImpotException e) {
                // display error
                string msg = e.InnerException == null ? null : String.Format(", Original exception: {0}", e.InnerException.Message);
                Console.WriteLine("The following error occurred: [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // terminate program
                Environment.Exit(1);
            }
            // infinite loop
            while (true) {
...

becomes the following in [Dialogue2.cs]:


            // we create the [business and DAO] layers
            IApplicationContext ctx = null;
            try {
                // Spring context
                ctx = ContextRegistry.GetContext();
            } catch (Exception e1) {
                // display error
                Console.WriteLine("Exception chain: \n{0}", "".PadLeft(40, '-'));
                Exception e = e1;
                while (e != null) {
                    Console.WriteLine("{0}: {1}", e.GetType().FullName, e.Message);
                    Console.WriteLine("".PadLeft(40, '-'));
                    e = e.InnerException;
                }
                // terminate program
                Environment.Exit(1);
            }
            // request a reference to the [business] layer
            IImpotMetier business = (IImpotMetier)ctx.GetObject("business");
            // infinite loop
            while (true) {
....................................
  • line 2: IApplicationContext provides access to all objects instantiated by Spring. This object is called the application’s Spring context or, more simply, the application context. At this point, this context has not yet been initialized. The following try/catch block performs this initialization.
  • line 5: the Spring configuration in [App.config] is read and processed. After this operation, if no exception occurred, all objects in the <objects> section have been instantiated:
  • the Spring "dao" object is an instance in the [dao] layer
  • the Spring object "metier" is an instance in the [metier] layer
  • Line 19: The [Dialogue2.cs] class requires a reference to the [business] layer. This is requested from the application context. The IApplicationContext object provides access to Spring objects via their names (the `name` attribute of the `<object>` tag in the Spring configuration). The returned reference is a reference to the generic type Object. We must cast the returned reference to the correct type, in this case the type of the [business] layer interface: IImpotMetier.

If everything went well, after line 19, [Dialogue2.cs] has a reference to the [business] layer. The code in lines 21 and beyond is that of the [Dialogue.cs] class already discussed.

  • Lines 6–17: Handling the exception that occurs when the Spring configuration file cannot be fully processed. There may be various reasons for this: incorrect syntax in the configuration file itself, or the inability to instantiate one of the configured objects. In our example, the latter case would occur if the DataImport.txt file referenced on line 17 of [App.config] were not found in the project’s runtime directory.

The exception thrown on line 6 is part of a chain of exceptions, where each exception has two properties:

  • Message: the error message associated with the exception
  • InnerException: the previous exception in the exception chain

The loop in lines 10–14 displays all exceptions in the chain in the following format: exception class and associated message.

When running the [ui] project with a valid configuration file, you get the usual results:

Tax calculation parameters in the format: Married (y/n) No. of Children Salary or nothing to stop at: 2 60000
Tax = 4,282 euros

When running the [ui] project with a non-existent [DataImpotInexistant.txt] file,


            <object name="dao" type="Dao.FileImpot, ImpotsV5-dao">
                <constructor-arg index="0" value="DataImpotInexistant.txt"/>
            </object>

the following results are obtained:

Exception chain:
----------------------------------------
System.Configuration.ConfigurationErrorsException: Error creating context 'spring.root': Could not find file 'C:\data\2007-2008\c# 2008\poly\Chap4\ImpotsV5\ui\bin\Release\DataImpotInexistant.txt'.
----------------------------------------
Spring.Util.FatalReflectionException: Cannot instantiate Type [Spring.Context.Support.XmlApplicationContext] using ctor [Void .ctor(System.String, Boolean, System.String[])]: 'Exception has been thrown by the target of an invocation.'
----------------------------------------
System.Reflection.TargetInvocationException: An exception has been thrown by the target of an invocation.
----------------------------------------
Spring.Objects.Factory.ObjectCreationException: Error creating object with name 'dao' defined in 'config [spring/objects]': Initialization of object failed: Cannot instantiate Type [Dao.FileImport] using ctor [Void.ctor(System.String)]: 'Exception has been thrown by the target of an invocation.'
----------------------------------------
Spring.Util.FatalReflectionException: Cannot instantiate Type [Dao.FileImport] using ctor [Void.ctor(System.String)]: 'Exception has been thrown by the target of an invocation.'
----------------------------------------
System.Reflection.TargetInvocationException: An exception has been thrown by the target of an invocation.
----------------------------------------
Entities.ImportException: Error reading the file DataImportNonExistent.txt
----------------------------------------
System.IO.FileNotFoundException: Could not find file 'C:\data\2007-2008\c# 2008\poly\Chap4\ImpotsV5\ui\bin\Release\DataImpotInexistant.txt'.
  • line 17: the original exception of type [FileNotFoundException]
  • line 15: the [dao] layer wraps this exception in a [Entites.ImpotException] type
  • line 9: the exception thrown by Spring because it failed to instantiate the object named "dao". During the creation of this object, two other exceptions occurred earlier: those on lines 11 and 13.
  • Because the "dao" object could not be created, the application context could not be created. This is the meaning of the exception on line 5. Previously, another exception, the one on line 7, had occurred.
  • Line 3: the top-level exception, the last in the chain: a configuration error is reported.

From all this, we can conclude that it is the deepest exception—in this case, the one on line 17—that is often the most significant. Note, however, that Spring has preserved the error message from line 17 and passed it up to the top-level exception on line 3 in order to identify the root cause of the error at the highest level.

Spring alone deserves a book. We have only scratched the surface here. You can explore it further with the document [spring-net-reference.pdf] found in the Spring installation folder:

 

You can also read [http://tahe.developpez.com/dotnet/springioc], a Spring tutorial presented in a VB.NET context.