Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Business Centric Architecture

3.11/5 (11 votes)
9 Jul 2008CPOL10 min read 1   388  
Design the business layer using a provider pattern

Introduction

When you start a new solution, you have to consider whether you want to expose the business layer as web services or WCF services or not at all. This exposure can add significant costs to the solution and from a business perspective it is not easy betting on reusability to justify these additional costs.

Another aspect is that the architecture upper levels should be agnostic regarding the technology used to supply data and services. They should work with business objects and interfaces (services) and be able to use any provider that implements the services contracts. It is up to the provider to worry about details on how to implement the services.

Diagrams

Let’s say you choose to expose services. You generally have two options:

  • The services layer is the business layer as well. Problem is, your UI will use the services even if the deployment is on a single box or even if no other enterprise application needs your services.
  • Your services layer exposes the services defined in the business layer. Problem is, you will have to write a lot of tedious code to expose the business layer and it just feels like writing the same thing again and again.

A classic tiered architecture using web services looks something like:
BusinessCentricArchitecture1.png

The Business Centric architecture looks like:
BusinessCentricArchitecture2.png

This shows that the UI or the upper levels only use business layer entities and interfaces. The UI is completely agnostic of the current provider implementing the business layer.

The picture does not show that the WCF and web services providers use the database provider to do the job. They are just shells that take care of the technicalities required by the technology being used. The providers’ rectangles are dotted meaning that there is no strong link between the UI and the providers. They are functional equivalent and interchangeable via configuration.

Why Business Centric?

Business Centric makes the “to expose or not to expose” choice a no brainer. This is because of the followings:

  • Exposing the business layer as a web service becomes almost insignificant in terms of costs and time.
  • The decision about whether to deploy the solution using web services or not can be made at any stage, as the switch between providers is done seamlessly via configuration.
  • Unit testing is easier, as testing the business layer is equivalent to testing any provider.
  • Because the plumbing code is generated, the business services comments get replicated in the factory classes, providers, web methods descriptions.
  • You can deploy the solution on a single box using the database provider if the number of users is small, and keep the web or WCF services only for external applications calls. If you need to spread the load you can later on deploy on other boxes and switch to web/WCF service.
  • The resulting solution does not reference any additional libraries. It is all based on core .NET and you can change every aspect of it if you wish to do so.

Brief Explanations

  • The business layer defines three types of entities: business exceptions, objects(entities) and interfaces (services). Every service has an associated factory class that instantiates a singleton to a business service provider instance based on configuration. The factory class exposes all the service’s methods as static methods that pass the call to the singleton.
  • You then define the business providers:
    • Database provider. The database provider will have to implement the business services based on a database back end. Data access plumbing code is auto generated from the database. You will have to manually write code for specific database operations and code for implementing the business services.
    • Web services provider. Will pass all the calls to a database provider and will auto generate all the plumbing code. The provider is actually a proxy class for a web service. The proxy class is auto generated as well. The proxy class will translate SOAP exceptions into business exceptions if appropriate.
    • WCF services provider. Same as the web services provider. The plumbing code will be different but still 100% auto-generated.
  • The UI knows nothing about the business provider. It only references the business layer and manipulates its entities. The UI’s configuration file specifies what provider to use for each service. The provider assembly has to be dropped in a location where it can be dynamically loaded.
  • The unit tests reference and test only the business layer. You can test all providers by manipulating the configuration.

Using the Code

Download the file from the links above that mathched your environment. Each one of the zip code files contains a folder named HelloBC. Extract this folder into c:\. The folder contains a solution named HelloBC.sln. Load the solution and run it, making sure that the UI\WebSite is the start-up project and the Default.aspx is the start page. Then follow the details outlined in the sections below.

The Business Project

1. Business exceptions: we start by defining the business exceptions. All business exceptions inherit from an exception class named BusinessException. This is needed in order to be able to define and catch all business exceptions. All business exceptions are serializable and XML serializable because we need to be able to send them across the wire to the proxy classes. The proxy classes will deserialize them and throw them as business exceptions so that they can be caught by the UI or the unit tests. The business exceptions are auto generated from an XSD schema as in the picture below.
BusinessCentricArchitecture3.png

2. Business objects: we then need to define what a business entity is (e.g. a customer). We can define them using XSD schemas as well:
BusinessCentricArchitecture4.png
This is how the definition looks. The generated classes will be adorned with the attributes required by web services or WCF. Here are some excerpts from the generated code:

C#
/// <summary>

/// Represents the gender of a customer.
/// </summary>
[System.CodeDom.Compiler.GeneratedCode("System.Xml", "2.0.0.0")]
[System.Serializable]
[System.Xml.Serialization.XmlType(Namespace="urn:HelloBC:Business1.Customers",
   TypeName="Gender")]
public enum Gender
{
    /// <summary>
    /// Represents the male gender.
    /// </summary>
    Male,
...

/// <summary>
/// Defines a customer as a business object.
/// </summary>

[System.CodeDom.Compiler.GeneratedCode("System.Xml", "2.0.0.0")]
[System.Serializable]
[System.ComponentModel.DesignerCategory("code")]
[System.ComponentModel.TypeConverter(typeof(
    System.ComponentModel.ExpandableObjectConverter))]
[System.Runtime.Serialization.DataContract(
    Namespace="urn:HelloBC:Business1.Customers", Name="Customer")]
[System.Xml.Serialization.XmlType(Namespace="urn:HelloBC:Business1.Customers",
    TypeName="Customer")]
public partial class Customer : Business1.BusinessObject
{
    /// <summary>
    /// The name of this customer.
    /// </summary>
    [System.Runtime.Serialization.DataMember(Order=1, Name="Name")]
    [System.Xml.Serialization.XmlElement(Order=1, ElementName="Name")]
    public System.String Name
    {
        get { return this._name; }
        set
        {
            if ((this._name != value))
            {
                this._name = value;
                this.RaisePropertyChanged("Name");
            }
        }
    } private System.String _name;
...

Note that the code comments are defined in the XSD schema as well, but the designer does not display them.

3. Business services: we then need to define the services provided by the business layer. The services are manually written interfaces exposing methods that manipulate business objects and eventually throw business exceptions when something goes wrong.

C#
public interface ICustomersService
{
    List<Customer> GetAll();
    Customer GetByID(int customerID);
    Customer Insert(Customer customer);
    Customer Update(Customer customer);
    void Delete(Customer customer);
}

The manually written interfaces automatically generate code that implements a factory class that exposes all the interface's methods as static methods that pass the call to a singleton. The singleton is instantiated according to the configuration to the current business provider (database, web service, WCF or any other).

The code below represents an excerpt from the generated factory class:

C#
[System.ComponentModel.DataObject]
public static class ICustomersServiceFactory
{
    public static ICustomersService Service
    {
        get { … }
        set { … }
    } private static ICustomersService _service = null;
    public static System.Collections.Generic.List<Business1.Customer> GetAll()
    {
        return Service.GetAll();
    }
    public static Business1.Customer GetByID(int customerID)
    {
        return Service.GetByID(customerID);
    }

Note that the code above does not contain the comments that are actually automatically migrated from the interface to the factory class.

The Database Provider

The database provider uses a Microsoft SQL Server database file named DesignDB.mdf that is used to define the database entities. We define in this database a table named Customers. Then we can use an XML file to generate table scripts and a data reader as in the example below:

XML
<TableDef xmlns="urn:HelloBC:RenderersLibrary1.Database.TableDef"
    ConnectionName="DatabaseProvider1.Properties.Settings.DesignDBConnectionString"
    Name="Customers" />

Here are some excerpts from the scripts and code generated from the database table in three separate files:

CREATE TABLE [dbo].[Customers]
(
    [CustomerID] int NOT NULL IDENTITY(1, 1),
    [Name] varchar(50) NOT NULL,
    ...
    [TimeStamp] timestamp NOT NULL
) ON [PRIMARY]
GO


CREATE PROCEDURE [dbo].[prc_CustomersGetByID]
(
    @CustomerID int
)
AS
BEGIN
    SET NOCOUNT ON
    SELECT
        [CustomerID],
        [Name],
        ...
        [TimeStamp]
    FROM [dbo].[Customers]
    WHERE
        [CustomerID] = @CustomerID
END
GO

C#
public partial class CustomersDataReader : DataReaderWrapper
{
    public enum Columns
    {
        CustomerID = 0,
        Name = 1,
        ...
    }
    public System.String Name
    {
        get
        {
            return (System.String)base.GetValue((int)Columns.Name);
        }
    }
    ...

You only need to manually write the implementation of the business services as in the example below:

C#
public class CustomersService : ICustomersService
{
    #region ICustomersService Members
    List<Customer> ICustomersService.GetAll()
    {
        List<Customer> customers = new List<Customer>();
        using (CustomersDataReader reader = CustomersDataReader.GetAll())
        {
            while (reader.Read())
            {
                customers.Add(LoadCustomer(reader));
            }
        }
        return customers;
    }

    private Customer LoadCustomer(CustomersDataReader reader)
    {
        Customer customer = new Customer(reader.CustomerID, reader.Name,
            reader.Age, (double)reader.Height,
            reader.IsMale ? Gender.Male : Gender.Female,
            reader.TimeStamp);
        customer.State = BusinessObjectState.Loaded;
        return customer;
    }

    Customer ICustomersService.GetByID(int customerID)
    {
        using (CustomersDataReader reader = CustomersDataReader.GetByID(customerID))
        {
            if (!reader.Read())
            {
                throw new RecordNotFoundException(
                    "Customer not found. Other user or process must have deleted it.",
                    customerID, this.GetType().Name);
            }
            return LoadCustomer(reader);
        }
    }

The Web Services Provider

The web services provider needs three projects: the web site that hosts the services, the implementation that does the job by passing the calls to the database provider and the proxy project.

1. The Web Services Implementation: exposes the business interfaces as web methods. The web methods transfer the calls to the current provider that normally is the database provider. The web methods catch the business exceptions and wrap them in SOAP exceptions.

XML
<ImplementationDef xmlns="urn:HelloBC:RenderersLibrary1.WebService.ImplementationDef"
                   InterfaceFullName="Business1.ICustomersService">
    <XmlNamespace>urn:HelloBC:WebService</XmlNamespace>

    <Name>CustomersService</Name>
    <BusinessExceptionClass>Business1.BusinessException</BusinessExceptionClass>
</ImplementationDef>

The XML file refers the manually written business interface. The generated code will look like:

C#
    [System.CodeDom.Compiler.GeneratedCode("System.Web.Services", "2.0.0.0")]    
    [WebService(Namespace="urn:HelloBC:WebService")]
    [WebServiceBinding(Namespace="urn:HelloBC:WebService", Name="CustomersService")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    public partial class CustomersService : BusinessWebService,
        Business1.ICustomersService
    {
        [WebMethod(Description=@"Gets a customer based on
an unique identifier.")]
        [SoapDocumentMethod("urn:HelloBC:WebService.GetByID",
                Use=SoapBindingUse.Literal, ParameterStyle=SoapParameterStyle.Bare)]
        [return: XmlElement(Namespace="urn:HelloBC:Business1.Customers",
            ElementName="Customer")]
        public Business1.Customer GetByID([XmlElement(Namespace="urn:HelloBC:WebService",
            ElementName="GetByIDcustomerID")] int customerID)
        {
            try
            {
                return Business1.ICustomersServiceFactory.GetByID(customerID);
            }
            catch (Business1.BusinessException businessException)
            {
                throw BuildBusinessSoapException(businessException);
            }
        }

As you can see in the code above, the call is passed to the business service factory class that will pass it to the database provider (as indicated by the web.config file in the web services site project). Please note that the business exception is caught and wrapped into a SOAP exception that is thrown instead.

2. The Web Services Site: this project is a very simple site that contains an ASMX file for each business service that uses the associated class defined in the web services implementation project.

This is the code for the CustomersService.asmx file:

XML
<%@ WebService Language="C#" Class="WebServiceImplementation1.CustomersService" %>

3. The Web Services Proxy: This project implements the proxy classes for the web services defined in the web services site. The proxy classes will also be business providers that can be used instead of the database providers by the UI or the unit tests. The UI and the unit tests projects will not create web references to the web services site. You will simply drop the web services proxy assembly DLL in the bin folder and change the configuration to use the web services provider. You don’t even have to reference the proxy assembly directly.

You define the proxy by using an XML file as outlined below:

XML
<ClientDef xmlns="urn:HelloBC:RenderersLibrary1.WebService.ClientDef"
    Path="$(SolutionDir)\Providers\WebService\WebServiceImplementation1\
    CustomersService.xml"/>

As you can see, the XML file refers the XML file from the implementation project and the generated code will look like:

C#
    [System.CodeDom.Compiler.GeneratedCode("System.Web.Services", "2.0.0.0")]    
    [DesignerCategory("code")]
    [WebService(Namespace="urn:HelloBC:WebService")]
    [WebServiceBinding(Namespace="urn:HelloBC:WebService", Name="CustomersService")]
    public partial class CustomersService : BaseWebServiceProxy,
        Business1.ICustomersService
    {
        [WebMethod(Description=@"Gets a customer based on
an unique identifier.")]
        [SoapDocumentMethod("urn:HelloBC:WebService.GetByID",
                Use=SoapBindingUse.Literal, ParameterStyle=SoapParameterStyle.Bare)]
        [return: XmlElement(Namespace="urn:HelloBC:Business1.Customers",
            ElementName="Customer")]
        public Business1.Customer GetByID([XmlElement(Namespace="urn:HelloBC:WebService",
            ElementName="GetByIDcustomerID")] int customerID)
        {
            try
            { 
                object[] results = this.Invoke("GetByID", new object[] {
                    customerID
                    });
                return (Business1.Customer)results[0];
            
            }
            catch (SoapException soapException)
            {
                throw base.ProcessSoapException(soapException);
            }
        }

The generated code is very similar to the one that you get when you add a web reference using the IDE with two major differences:

  • The proxy class implements the business service and uses the objects defined in the business project instead of redefining them.
  • The SOAP exceptions are caught and filtered. If they originated from a business exception they are translated and thrown as that business exception that will be caught as such by the UI or the unit tests.

These two very important differences make the proxy to qualify as a business provider and the solution can therefore switch transparently to using web services.

The WCF Services Provider

The WCF services provider is very similar to the web services provider. It needs three projects: the site, the implementation and the proxy. It differs to the web services provider only in technicalities required by WCF as opposed to web services.

WCF is closer to Business Centric than web services. It comes natural to build them around interfaces that represent business interfaces.

The UI

The UI uses its configuration file to specify which provider to use. It then uses the services and the objects provided by the business without being aware of the actual implementation or the business provider.

This is an example of a configuration that uses the database provider:

XML
<configSections>

    <section name="Business1.ICustomersService"
        type="Business1.ICustomersServiceConfigurationSection, Business1"/>
</configSections>
<Business1.ICustomersService type="DatabaseProvider1.CustomersService,
    DatabaseProvider1" />

And this is an example of a configuration that uses the web services providers:

XML
<configSections>
    <section name="Business1.ICustomersService"
        type="WebServiceProvider1.ICustomersServiceConfigurationSection,
        WebServiceProvider1" />
</configSections>
<Business1.ICustomersService type="WebServiceProvider1.CustomersService,
    WebServiceProvider1">
    <WebService url="http://localhost:2010/WebServicesSite1/CustomersService.asmx" />
</Business1.ICustomersService>

This is the only change required to switch the UI from the database provider to the web services provider. For WCF the configuration sections change will be even smaller, but you need to configure WCF as you would normally do.

You can then write a simple form with an editable data grid like in the code below:

HTML
<form id="form1" runat="server">

    <asp:GridView ID="_customersGrid" runat="server" AutoGenerateColumns="False"
            DataSourceID="_customersDataSource"
            AllowPaging="True" DataKeyNames="CustomerID,TimeStamp">
        <Columns>
            <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
            <asp:BoundField DataField="CustomerID" HeaderText="CustomerID"
                SortExpression="CustomerID" ReadOnly="True" />
            <asp:BoundField DataField="Name" HeaderText="Name"
                SortExpression="Name" />
            ...
            <asp:BoundField DataField="TimeStamp" HeaderText="TimeStamp"
                SortExpression="TimeStamp" ReadOnly="True" Visible="False" />

        </Columns>
    </asp:GridView>
    <asp:ObjectDataSource ID="_customersDataSource" runat="server"
        OldValuesParameterFormatString="original_{0}"
        SelectMethod="GetAll" InsertMethod="Insert" UpdateMethod="Update"
        DeleteMethod="Delete"
        TypeName="Business1.ICustomersServiceFactory"
        DataObjectTypeName="Business1.Customer">
    </asp:ObjectDataSource>
</form>

As you can see, this is a form containing a grid view bound to an object data source. The data source uses the static methods exposed by the factory class that pass the call to the current provider as specified in the web.config file.

Final Note

The resulting solution is based on standard .NET functionality only. You have no dependencies on a framework and you own absolutely all aspects of the implemented functionality.

The solution attempts to model the business view of the world as much as possible using standard tools (XML, XSD, manually written code, Excel and databases). Non programmers can therefore have a meaningfull contribution to building the solution as long as their output is structured. The usage of code generation can yield excellent results in terms of productivity.

Consequently you are enabled to building a flexible business centric, services orientated solution with considerably reduced efforts associated with plumbing code.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)