Building applications with WCF - Part 1 of n
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
- Profile: For creating a new bank customer, getting details about existing customer
- 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
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.
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; }
}
}
using System.Data.Entity;
namespace BankDAL.Model
{
public class BankDbContext: DbContext
{
public DbSet<Customer> Customers { get; set; }
}
}
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);
}
}
}
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.
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.
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();
}
}
}
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);
}
}
}
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);
}
}
}
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