 |
|
 |
Introduction |
|
Chapter II |
The Series
WCF by example is a series of articles that describe how to design and develop a WPF client using WCF for communication and NHibernate for persistence purposes. The series introduction describes the scope of the articles and the project.
Chapter Overview
This is the first chapter of the series. This chapter sets the baseline for the eDirectory solution; we will introduce some core solution concepts as the Customer
entity, the definition of the repository, a customer service, and our first couple tests.
The source code for this chapter can be found at CodePlex change set 67232. The latest code for the eDirectory solution is found at CodePlex.
The Model - Customer Entity
As we mentioned in our introduction, we will keep our model very simple, only one entity for the moment will be required. The customer
class is declared as:
public class Customer
{
protected Customer() { }
public long Id { get; protected set; }
public string FirstName { get; protected set; }
public string LastName { get; protected set; }
public string Telephone { get; protected set; }
}
Couple things to observed; properties are protected
so they can only be changed by action methods and the class does not provide a public
constructor. We should not assume that our classes are going to be used in the proper way so making the constructor protected
provides a nice way to controll the creation of new instances. We will justify this approach when we cover persistence in a later chapter.
Also, entities are defined in the eDirectory.Domain
assembly which will be used only by our server side components; however, the client does not use the domain classes.
The Customer Services
The eDirectory provides the following customer services:
Function |
Description |
GetById |
Retrieves a customer by providing a unique ID |
ListAll |
Returns all customer instances |
CreateNewCustomer |
Creates a new customer instance |
UpdateCustomer |
Amends an existing customer instance |
Services are defined using WCF conventions. Input parameters will be primitives or data transfer objects; all the service methods are functions. We must keep our service signatures simple to avoid serialisation problems with WCF. We also want to provide a communication pattern between the client and server without relying on a specific WCF mechanism, we will see in later chapters how this aspect is resolved.
Also, service contracts (interfaces) are declared in a common assembly as both the server and client components require them. The same is true for the DTOs.
At this stage, we need our first DTO which looks very similar to our Customer
entity:
public class CustomerDto
{
public long CustomerId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Telephone { get; set; }
}
Now, we can define our first service contract:
[ServiceContract(Namespace = "http://wcfbyexample/customerservices/")]
public interface ICustomerService
{
[OperationContract]
CustomerDto CreateNewCustomer(CustomerDto customer);
[OperationContract]
CustomerDto GetById(long id);
[OperationContract]
CustomerDto UpdateCustomer(CustomerDto customer);
[OperationContract]
List<customerdto> FindAll();
}
Let's try to create a mechanism for creating customer instances.
Repository
Domain entities should be decoupled from the persistence back-end mechanism. The repository pattern provides the required persistence functionality to our domain entities.
At this point, the following repository definition is sufficient:
public interface IRepository<tentity />
{
#region CRUD operations
TEntity Save(TEntity instance);
void Update(TEntity instance);
void Remove(TEntity instance);
#endregion
#region Retrieval Operations
TEntity GetById(long id);
IQueryable<tentity /> FindAll();
#endregion
}
The most important aspects to notice are that we have defined a generic repository and that the FindAll
method returns an IQueryable
instance.
Create Factory Method
At this point, we can go back to our domain class and provide an easy mechanism for creating customer instances:
public static Customer Create(IRepository<customer /> repository, CustomerDto operation)
{
var instance = new Customer
{
FirstName = operation.FirstName,
LastName = operation.LastName,
Telephone = operation.Telephone
};
repository.Save(instance);
return instance;
}
See how the DTO instance is used to populate the properties of a new customer instance. Then the save
method is executed against the passed repository. Instead of using a reference to the repository implementation in our entities, we will pass an instance of the repository to our entity methods and factories. This approach will evolve as we cover future aspects of the application. Another aspect to notice here is that the factory method creates persisted customer instances.
In-Memory Repository Implementation
At this point, we could start doing some TDD, we could mock repositories and so on, but providing an in-memory repository implementation is relatively inexpensive and leverages testing and business exploration. As the In-Memory persistor will never be used in a production environment, we want to declare this implementation in a different assembly: eDirectory.Naive
. We define an abstract class for in-memory persistors with the common functionality:
public abstract class RepositoryEntityStore<tentity>
:IRepository<tentity>
{
protected readonly IDictionary<long,> RepositoryMap = new Dictionary<long,>();
#region IRepository<tentity> Members
public abstract TEntity Save(TEntity instance);
public abstract void Update(TEntity instance);
public abstract void Remove(TEntity instance);
public TEntity GetById(long id)
{
return RepositoryMap[id];
}
public IQueryable<tentity> FindAll()
{
return RepositoryMap.Values.AsQueryable();
}
#endregion
}
Then we create the Customer
implementation:
public class RepositoryCustomer
: RepositoryEntityStore<customer>
{
public override Customer Save(Customer instance)
{
...
}
public override void Update(Customer instance)
{
...
}
public override void Remove(Customer instance)
{
...
}
private void GetNewId(Customer instance)
{
...
}
private readonly IDictionary<type,> Setters = new Dictionary<type,>();
private MethodInfo GetSetter(Type type)
{
...
}
}
There is plenty of room for improvement in the above code, but it is worth noting how the entities are stored in a hashed map using the entity ID. As I mentioned in the series introduction, we have full control over our database design so we have chosen to have a numeric primary key in all our tables which eases the domain entities definition. In later chapters, we will see how the above code evolves and one single implementation for all entity types alone is required.
Our First Test
Let's create a customer using the factory and then we check that the ID and first name are correct:
[TestClass]
public class CustomerTests
{
[TestMethod]
public void CreateCustomer()
{
var repository = new RepositoryCustomer();
var dto = new CustomerDto
{
FirstName = "Joe",
LastName = "Bloggs",
Telephone = "9999-8888"
};
var customer = Customer.Create(repository, dto);
Assert.IsFalse(customer.Id == 0, "Customer Id should have been updated");
Assert.AreSame(customer.FirstName, dto.FirstName, "First Name are different");
}
}
Customer Service Implementation
We are now ready to create our first service implementation:
public class CustomerService
:ICustomerService
{
public IRepository<customer> Repository { get; set; }
#region ICustomerService Members
public Common.Dto.Customer.CustomerDto CreateNewCustomer(CustomerDto dto)
{
var customer = Customer.Create(Repository, dto);
return new CustomerDto
{
CustomerId = customer.Id,
FirstName = customer.FirstName,
LastName = customer.LastName,
Telephone = customer.Telephone
};
}
...
}
Let's create a test that executes this service:
[TestClass]
public class CustomerServiceTests
{
[TestMethod]
public void CreateCustomer()
{
var service = new CustomerService { Repository = new RepositoryCustomer() };
var dto = new CustomerDto
{
FirstName = "Joe",
LastName = "Bloggs",
Telephone = "9999-8888"
};
var customer = service.CreateNewCustomer(dto);
Assert.IsFalse(customer.CustomerId == 0,
"Customer Id should have been updated");
Assert.AreSame(customer.FirstName, dto.FirstName,
"First Name are different");
}
}
Chapter Conclusion
We have covered a lot of aspects in this article. In the first place, we set up a few projects: Common, Domain, and Naive. We described the basics about designing domain entities and their relation to the repository. We concluded the chapter using the service to create a customer instance and wrote our first couple tests.
In the next chapter, we will discuss some of the proposed patterns used so far, we will identify a few aspects that could be improved, and we will introduce the Repository Locator concept.