Building applications with WCF - Intro

Posted by skjagini on ASP.net Weblogs See other posts from ASP.net Weblogs or by skjagini
Published on Sun, 09 Jan 2011 22:30:00 GMT Indexed on 2011/01/10 20:54 UTC
Read the original article Hit count: 467

Filed under:
|
|

I am going to write series of articles using Windows Communication Framework (WCF) to develop client and server applications and this is the first part of that series.

What is WCF

As Juwal puts in his Programming WCF book, WCF provides an SDK for developing and deploying services on Windows, provides runtime environment to expose CLR types as services and consume services as CLR types. Building services with WCF is incredibly easy and it’s implementation provides a set of industry standards and off the shelf plumbing including service hosting, instance management, reliability, transaction management, security etc such that it greatly increases productivity

Scenario:

Lets consider a typical bank customer trying to create an account, deposit amount and transfer funds between accounts, i.e. checking and savings. To make it interesting, we are going to divide the functionality into multiple services and each of them working with database directly. We will run test cases with and without transactional support across services.

In this post we will build contracts, services, data access layer, unit tests to verify end to end communication etc, nothing big stuff here and we dig into other features of the WCF in subsequent posts with incremental changes.

In any distributed architecture we have two pieces i.e. services and clients. Services as the name implies provide functionality to execute various pieces of business logic on the server, and clients providing interaction to the end user. Services can be built with Web Services or with WCF. Service built on WCF have the advantage of binding independent, i.e. can run against TCP and HTTP protocol without any significant changes to the code.

Solution

  • Services
    • Profile: For creating a new bank customer, getting details about existing customer
      • ProfileContract
      • ProfileService
    • Checking Account: To get checking account balance, deposit or withdraw amount
      • CheckingAccountContract
      • CheckingAccountService
    • Savings Account: To get savings account balance, deposit or withdraw amount
      • SavingsAccountContract
      • SavingsAccountService
  • ServiceHost: To host services, i.e. running the services at particular address, binding and contract where client can connect to
  • Client: Helps end user to use services like creating account and amount transfer between the accounts
  • BankDAL: Data access layer to work with database

 

image

 

BankDAL

It’s no brainer not to use an ORM as many matured products are available currently in market including Linq2Sql, Entity Framework (EF), LLblGenPro etc. For this exercise I am going to use Entity Framework 4.0, CTP 5 with code first approach.

There are two approaches when working with data, data driven and code driven. In data driven we start by designing tables and their constrains in database and generate entities in code while in code driven (code first) approach entities are defined in code and the metadata generated from the entities is used by the EF to create tables and table constrains.

In previous versions the entity classes had  to derive from EF specific base classes. In EF 4 it  is not required to derive from any EF classes, the entities are not only persistence ignorant but also enable full test driven development using mock frameworks. 

Application consists of 3 entities, Customer entity which contains Customer details; CheckingAccount and SavingsAccount to hold the respective account balance. We could have introduced an Account base class for CheckingAccount and SavingsAccount which is certainly possible with EF mappings but to keep it simple we are just going to follow 1 –1 mapping between entity and table mappings.

image

Lets start out by defining a class called Customer which will be mapped to Customer table, observe that the class is simply a plain old clr object (POCO) and has no reference to EF at all.

using System;
 
namespace BankDAL.Model
{
    public class Customer
    {
        public int Id { get; set; }
        public string FullName { get; set; }
        public string Address { get; set; }
        public DateTime DateOfBirth { get; set; }
    }
}
 
In order to inform EF about the Customer entity we have to define a database context with properties of type DbSet<> for every POCO which needs to be mapped to a table in database. EF uses convention over configuration to generate the metadata resulting in much less configuration.
using System.Data.Entity;
 
namespace BankDAL.Model
{
    public class BankDbContext: DbContext
    {
        public DbSet<Customer> Customers { get; set; }
 
    }
}
 
Entity constrains can be defined through attributes on Customer class or using fluent syntax (no need to muscle with xml files), CustomerConfiguration class. By defining constrains in a separate class we can maintain clean POCOs without corrupting entity classes with database specific information.
 
using System;
using System.Data.Entity.ModelConfiguration;
 
namespace BankDAL.Model
{
    public class CustomerConfiguration: EntityTypeConfiguration<Customer>
    {
        public CustomerConfiguration()
        {
            Initialize();
        }
 
        private void Initialize()
        {
            //Setting the Primary Key
            this.HasKey(e => e.Id);
 
            //Setting required fields
            this.HasRequired(e => e.FullName);
            this.HasRequired(e => e.Address);
            //Todo: Can't create required constraint as DateOfBirth is not reference type, research it
            //this.HasRequired(e => e.DateOfBirth);
        }
    }
}
 
Any queries executed against Customers property in BankDbContext are executed against Cusomers table. By convention EF looks for connection string with key of BankDbContext when working with the context.
 
We are going to define a helper class to work with Customer entity with methods for querying, adding new entity etc and these are known as repository classes, i.e., CustomerRepository
 
using System;
using System.Data.Entity;
using System.Linq;
using BankDAL.Model;
 
namespace BankDAL.Repositories
{
    public class CustomerRepository
    {
        private readonly IDbSet<Customer> _customers;
 
        public CustomerRepository(BankDbContext bankDbContext)
        {
            if (bankDbContext == null) throw new ArgumentNullException();
            _customers = bankDbContext.Customers;
        }
 
        public IQueryable<Customer> Query()
        {
            return _customers;
        }
 
        public void Add(Customer customer)
        {
            _customers.Add(customer);
        }
    }
}
 
From the above code it is observable that the Query methods returns customers as IQueryable i.e. customers are retrieved only when actually used i.e. iterated. Returning as IQueryable also allows to execute filtering and joining statements from business logic  using lamba 
expressions without cluttering the data access layer with tens of methods.
 

Our CheckingAccountRepository and SavingsAccountRepository look very similar to each other

using System;
using System.Data.Entity;
using System.Linq;
using BankDAL.Model;
 
namespace BankDAL.Repositories
{
    public class CheckingAccountRepository
    {
        private readonly IDbSet<CheckingAccount> _checkingAccounts; 
 
        public CheckingAccountRepository(BankDbContext bankDbContext)
        {
            if (bankDbContext == null) throw new ArgumentNullException();
           _checkingAccounts = bankDbContext.CheckingAccounts;
        }
 
        public IQueryable<CheckingAccount> Query()
        {
            return _checkingAccounts;
        }
 
        public void Add(CheckingAccount account)
        {
            _checkingAccounts.Add(account);
        }
 
        public IQueryable<CheckingAccount> GetAccount(int customerId)
        {
            return (from act in _checkingAccounts
                    where act.CustomerId == customerId
                    select act);
        }
 
      
    }
}

The repository classes look very similar to each other for Query and Add methods, with the help of C# generics and implementing repository pattern (Martin Fowler) we can reduce the repeated code. Jarod from ElegantCode has posted an article on how to use repository pattern with EF which we will implement in the subsequent articles along with WCF Unity life time managers by Drew

Contracts

It is very easy to follow contract first approach with WCF, define the interface and append ServiceContract, OperationContract attributes.

IProfile contract exposes functionality for creating customer and getting customer details.

image 

using System;
using System.ServiceModel;
using BankDAL.Model;
 
namespace ProfileContract
{
    [ServiceContract]
    public interface IProfile
    {
        [OperationContract]
        Customer CreateCustomer(string customerName, string address, DateTime dateOfBirth);
 
        [OperationContract]
        Customer GetCustomer(int id);
 
    }
}
 
ICheckingAccount contract exposes functionality for working with checking account, i.e., getting balance, deposit and withdraw of amount. 
ISavingsAccount contract looks the same as checking account. 
 
using System.ServiceModel;
 
namespace CheckingAccountContract
{
    [ServiceContract]
    public interface ICheckingAccount
    {
        [OperationContract]
        decimal? GetCheckingAccountBalance(int customerId);
 
        [OperationContract]
        void DepositAmount(int customerId,decimal amount);
 
        [OperationContract]
        void WithdrawAmount(int customerId, decimal amount);
 
    }
}
 
Services
 
Having covered the data access layer and contracts so far and here comes the core of the business logic, i.e. services.   

ProfileService implements the IProfile contract for creating customer and getting customer detail using CustomerRepository.

image

using System;
using System.Linq;
using System.ServiceModel;
using BankDAL;
using BankDAL.Model;
using BankDAL.Repositories;
using ProfileContract;
 
namespace ProfileService
{
    [ServiceBehavior(IncludeExceptionDetailInFaults = true)]
    public class Profile: IProfile
    {
        public Customer CreateAccount(
            string customerName, string address, DateTime dateOfBirth)
        {
            Customer cust = 
                new Customer 
                { FullName = customerName, Address = address, DateOfBirth = dateOfBirth };
 
            using (var bankDbContext = new BankDbContext())
            {
                new CustomerRepository(bankDbContext).Add(cust);
                bankDbContext.SaveChanges();
            }
            return cust;
        }
 
        public Customer CreateCustomer(string customerName, string address, DateTime dateOfBirth)
        {
            return CreateAccount(customerName, address, dateOfBirth);
        }
        
        public Customer GetCustomer(int id)
        {
            return new CustomerRepository(new BankDbContext()).Query()
                .Where(i => i.Id == id).FirstOrDefault();
        }
 
    }
}
From the above code you shall observe that we are calling bankDBContext’s SaveChanges method and there is no save method specific to customer entity because EF manages all the changes centralized at the context level and all the pending changes so far are submitted in a batch and it is represented as Unit of Work.

Similarly Checking service implements ICheckingAccount contract using CheckingAccountRepository, notice that we are throwing overdraft exception if the balance falls by zero. WCF has it’s own way of raising exceptions using fault contracts which will be explained in the subsequent articles.

SavingsAccountService is similar to CheckingAccountService.

using System;
using System.Linq;
using System.ServiceModel;
using BankDAL.Model;
using BankDAL.Repositories;
using CheckingAccountContract;
 
namespace CheckingAccountService
{
    [ServiceBehavior(IncludeExceptionDetailInFaults = true)]
    public class Checking:ICheckingAccount
    {
        public decimal? GetCheckingAccountBalance(int customerId)
        {
            using (var bankDbContext = new BankDbContext())
            {
                CheckingAccount account =
                    (new CheckingAccountRepository(bankDbContext)
                    .GetAccount(customerId)).FirstOrDefault();
 
                if (account != null)
                    return account.Balance;
 
                return null;
            }
            
        }
 
        public void DepositAmount(int customerId, decimal amount)
        {
            using(var bankDbContext = new BankDbContext())
            {
                var checkingAccountRepository = 
                    new CheckingAccountRepository(bankDbContext);
                CheckingAccount account = 
                    (checkingAccountRepository.GetAccount(customerId))
                    .FirstOrDefault();
 
                if (account == null)
                {
                    account = new CheckingAccount() { CustomerId = customerId };
                    checkingAccountRepository.Add(account);
                }
 
                account.Balance = account.Balance + amount;
                if (account.Balance < 0)
                    throw new ApplicationException("Overdraft not accepted");
 
                bankDbContext.SaveChanges();
            }
        }
        
        public void WithdrawAmount(int customerId, decimal amount)
        {
           DepositAmount(customerId, -1*amount);
        }
    }
}
 
BankServiceHost
The host acts as a glue binding contracts with it’s services, exposing the endpoints. The services can be exposed either through the code or 
configuration file,  configuration file is preferred as it allows run time changes to service behavior even after deployment.
We have 3 services and for each of the service you need to define name (the class that implements the service with fully qualified namespace) 
and endpoint known as ABC, i.e. address, binding and contract. We are using netTcpBinding and have defined the base address
with for each of the contracts
<system.serviceModel>    
  <services>
    <service name="ProfileService.Profile">
      <endpoint binding="netTcpBinding" contract="ProfileContract.IProfile"/>
      <host>
        <baseAddresses>
          <add baseAddress="net.tcp://localhost:1000/Profile"/>
        </baseAddresses>
      </host>        
    </service>
    <service name="CheckingAccountService.Checking">
      <endpoint binding="netTcpBinding" 
                contract="CheckingAccountContract.ICheckingAccount"/>
      <host>
        <baseAddresses>
          <add baseAddress="net.tcp://localhost:1000/Checking"/>
        </baseAddresses>
      </host>
    </service>
    <service name="SavingsAccountService.Savings">
      <endpoint binding="netTcpBinding" 
                contract="SavingsAccountContract.ISavingsAccount"/>
      <host>
        <baseAddresses>
          <add baseAddress="net.tcp://localhost:1000/Savings"/>
        </baseAddresses>
      </host>
    </service>
  </services>
</system.serviceModel>

Have to open the services by creating service host which will handle the incoming requests from clients.
 
using System;
 
namespace ServiceHost
{
    class Program
    {
        static void Main(string[] args)
        {
            CreateHosts();
            Console.ReadLine();
        }
 
        private static void CreateHosts()
        {
            CreateHost(typeof(ProfileService.Profile),"Profile Service");
            CreateHost(typeof(SavingsAccountService.Savings), 
                "Savings Account Service");
            CreateHost(typeof(CheckingAccountService.Checking), 
                "Checking Account Service");
        }
 
        private static void CreateHost(Type type, string hostDescription)
        {
            System.ServiceModel.ServiceHost host = 
                new System.ServiceModel.ServiceHost(type);
            host.Open();
 
            if (host.ChannelDispatchers != null
                && host.ChannelDispatchers.Count != 0
                && host.ChannelDispatchers[0].Listener != null)
                Console.WriteLine("Started: " + host.ChannelDispatchers[0].Listener.Uri);
            else
                Console.WriteLine("Failed to start:" + hostDescription);
        }
    }
}
BankClient
image 
 
The client has no knowledge about service business logic other than the functionality it exposes through the contract, end points and a proxy 
to work against. The endpoint data and server proxy can be generated by right clicking on the project reference and choosing 
Add Service Reference’ and entering the service end point address. Or if you have access to source, you can manually reference 
contract dlls and update clients configuration file to point to the service end point if the server and client happens to be being built 
using .Net framework. One of the pros with the manual approach is you don’t have to work against messy code generated files. 
 
<system.serviceModel>
  <client>
    
    <endpoint name="tcpProfile" address="net.tcp://localhost:1000/Profile" binding="netTcpBinding" contract="ProfileContract.IProfile"/>
    
    <endpoint name="tcpCheckingAccount" address="net.tcp://localhost:1000/Checking" binding="netTcpBinding" contract="CheckingAccountContract.ICheckingAccount"/>
    
    <endpoint name="tcpSavingsAccount" address="net.tcp://localhost:1000/Savings" binding="netTcpBinding" contract="SavingsAccountContract.ISavingsAccount"/>
 
  </client>
</system.serviceModel>
The client uses a façade to connect to the services 
 
using System.ServiceModel;
using CheckingAccountContract;
using ProfileContract;
using SavingsAccountContract;
 
namespace Client
{
    public class ProxyFacade
    {
        public static IProfile ProfileProxy()
        {
            return 
                (new ChannelFactory<IProfile>("tcpProfile")).CreateChannel();
        }
 
        public static ICheckingAccount CheckingAccountProxy()
        {
            return (new ChannelFactory<ICheckingAccount>("tcpCheckingAccount"))
                .CreateChannel();
        }
 
        public static ISavingsAccount SavingsAccountProxy()
        {
            return (new ChannelFactory<ISavingsAccount>("tcpSavingsAccount"))
                .CreateChannel();
        }
 
    }
}
 
With that in place, lets get our unit tests going
 
using System;
using System.Diagnostics;
using BankDAL.Model;
using NUnit.Framework;
using ProfileContract;
 
namespace Client
{
    [TestFixture]
    public class Tests
    {
        private void TransferFundsFromSavingsToCheckingAccount(int customerId, 
            decimal amount)
        {
            ProxyFacade.CheckingAccountProxy().DepositAmount(customerId, amount);
            ProxyFacade.SavingsAccountProxy().WithdrawAmount(customerId, amount);
        }
 
        private void TransferFundsFromCheckingToSavingsAccount(int customerId, 
            decimal amount)
        {
            ProxyFacade.SavingsAccountProxy().DepositAmount(customerId, amount);
            ProxyFacade.CheckingAccountProxy().WithdrawAmount(customerId,  amount);
        }
 
 
        [Test]
        public void CreateAndGetProfileTest()
        {
            IProfile profile = ProxyFacade.ProfileProxy();
            const string customerName = "Tom";
            int customerId = profile.CreateCustomer(customerName, "NJ", 
                new DateTime(1982, 1, 1)).Id;
            Customer customer = profile.GetCustomer(customerId);
            Assert.AreEqual(customerName,customer.FullName);
        }
 
        [Test]
        public void DepositWithDrawAndTransferAmountTest()
        {
            IProfile profile = ProxyFacade.ProfileProxy();
            string customerName = "Smith" + DateTime.Now.ToString("HH:mm:ss");
            var customer  = profile.CreateCustomer(customerName, "NJ", 
                new DateTime(1982, 1, 1));
            
            // Deposit to Savings
            ProxyFacade.SavingsAccountProxy().DepositAmount(customer.Id, 100);
            ProxyFacade.SavingsAccountProxy().DepositAmount(customer.Id, 25);
            Assert.AreEqual(125, 
                ProxyFacade.SavingsAccountProxy().GetSavingsAccountBalance(customer.Id));
            // Withdraw
            ProxyFacade.SavingsAccountProxy().WithdrawAmount(customer.Id,  30);
            Assert.AreEqual(95,
                ProxyFacade.SavingsAccountProxy().GetSavingsAccountBalance(customer.Id));
 
            // Deposit to Checking
            ProxyFacade.CheckingAccountProxy().DepositAmount(customer.Id, 60);
            ProxyFacade.CheckingAccountProxy().DepositAmount(customer.Id, 40);
            Assert.AreEqual(100,
                ProxyFacade.CheckingAccountProxy().GetCheckingAccountBalance(customer.Id));
            // Withdraw
            ProxyFacade.CheckingAccountProxy().WithdrawAmount(customer.Id,  30);
            Assert.AreEqual(70,
                ProxyFacade.CheckingAccountProxy().GetCheckingAccountBalance(customer.Id));
 
            // Transfer from Savings to Checking
            TransferFundsFromSavingsToCheckingAccount(customer.Id,10);
            Assert.AreEqual(85,
                ProxyFacade.SavingsAccountProxy().GetSavingsAccountBalance(customer.Id));
            Assert.AreEqual(80, 
                ProxyFacade.CheckingAccountProxy().GetCheckingAccountBalance(customer.Id));
 
            // Transfer from Checking to Savings
            TransferFundsFromCheckingToSavingsAccount(customer.Id, 50);
            Assert.AreEqual(135, 
                ProxyFacade.SavingsAccountProxy().GetSavingsAccountBalance(customer.Id));
            Assert.AreEqual(30, 
                ProxyFacade.CheckingAccountProxy().GetCheckingAccountBalance(customer.Id));
        }
 
        [Test]
        public void FundTransfersWithOverDraftTest()
        {
            IProfile profile = ProxyFacade.ProfileProxy();
            string customerName = "Angelina" + DateTime.Now.ToString("HH:mm:ss");
 
            var customerId = 
                profile.CreateCustomer(customerName, "NJ", new DateTime(1972, 1, 1)).Id;
 
            ProxyFacade.SavingsAccountProxy().DepositAmount(customerId, 100);
            TransferFundsFromSavingsToCheckingAccount(customerId,80);
            Assert.AreEqual(20, 
                ProxyFacade.SavingsAccountProxy().GetSavingsAccountBalance(customerId));
            Assert.AreEqual(80, 
                ProxyFacade.CheckingAccountProxy().GetCheckingAccountBalance(customerId));
 
            try
            {
                TransferFundsFromSavingsToCheckingAccount(customerId,30);
            }
            catch (Exception e)
            {
                Debug.WriteLine(e.Message);
            }
 
            Assert.AreEqual(110, 
                ProxyFacade.CheckingAccountProxy().GetCheckingAccountBalance(customerId));
            Assert.AreEqual(20, 
                ProxyFacade.SavingsAccountProxy().GetSavingsAccountBalance(customerId));
            
        }
    }
}

 

We are creating a new instance of the channel for every operation, we will look into instance management and how creating a new instance of channel affects it in subsequent articles.

The first two test cases deals with creation of Customer, deposit and withdraw of month between accounts. The last case, FundTransferWithOverDraftTest() is interesting. Customer starts with depositing $100 in SavingsAccount followed by transfer of $80 in to checking account resulting in $20 in savings account.  Customer then initiates $30 transfer from Savings to Checking resulting in overdraft exception on Savings with $30 being deposited to Checking. As we are not running both the requests in transactions the customer ends up with more amount than what he started with $100. In subsequent posts we will look into transactions handling. 

Make sure the ServiceHost project is set as start up project and start the solution. Run the test cases either from NUnit client or TestDriven.Net/Resharper which ever is your favorite tool. Make sure you have updated the data base connection string in the ServiceHost config file to point to your local database

© ASP.net Weblogs or respective owner

Related posts about wcf

Related posts about orm