| | |
Chapter IV | | Chapter VI |
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 discusses the architect solution at a high level.
Sharpy - A Metro Project
23 Oct: I am currently working on a new project for a Metro application: Sharpy. I intend to use the patterns discussed in WCF by Example for the service side of the application; the goal is to demonstrate how similar is the development of Metro applications to the type of applications we have seen so far. There is not code available yet but hopefully this will change soon. I hope you like it. The article is in here.
Chapter Overview
In "Chapter I - Baseline", a draft version of the CustomerService
was defined. In "Chapter III - Response", the service was re-factored so business warnings and exceptions are always available at the client side. In "Chapter IV - Transaction Manager", we discussed the need for managing transactions, warnings and exceptions; two components were introduced: the Transaction Manager Factory and the ServiceBase. We did not completely resolve how business warnings are logged during the business logic processing.
Currently our services have a reference to an instance of the Transaction Manager Factory, a different implementation of the factory is used depending on whether the application is running in-memory or NHibernate mode. This technical requirement is normally resolved using dependency injection. However there is no need for having a container managing the creation of the services when a single instance of the factory fulfills the requirement, even in a multi-request scenario. As a result, we will use a different approach, we will provide a global context so the server components can gain access to a single instance of the factory. In a later chapter, we will see how DI is used to determine the factory implementation exposed by the Global Context.
We also indicated in previous chapter that there is a need for a sort of mechanism so services and business logic can record business warnings. In Chapter IV, a mechanism was defined on the transaction manager that checks if business warnings were created during the business processing. We will see how this aspect is implemented in this chapter by means of the BusinessNotifier
, a new component that we are introducing in this chapter.
As a result of a new type of services that are required across the server layers, we will discuss the need for a container of "vertical" services. Some of these services are created once for the whole life of the application where others are created only for the request duration.
The source code for this chapter can be found at Codeplex change set 73565. The latest code for the eDirectory solution is found at Codeplex.
Contexts
As with traditional web applications and/or web services, we deal with incoming requests that require to access services and data in an isolated fashion, these services and data are created during the request processing and must be terminated when the request is serviced. In WCF, the Instance Context represents the context information for a service instance and it can be used to provide exactly what we need for our request requirements. We will see in a later chapter the implementation of the request context using WCF extensions. For the time, in this chapter we will define the baseline for our container and contexts.
Global Context
The Global context exposes classes and data that can be used by our server components, they are request agnostic and are kept alive along with the application. A good approach with these global resources and services is not keeping any sort of state if feasible, concurrency issues may arise if the design is not correct. Locking mechanisms may help with the concurrency but may incur in performance degradation.
The Transaction Manager factory is an ideal candidate for being stored in the global context. The factory class does not manage any state and one single instance satisfy the application requirements for the creation of transaction managers even in a multi-request scenario.
public interface IGlobalContext
{
ITransFactory TransFactory { get; }
}
A single implementation of the IGlobalContext
is required:
public class GlobalContext
: IGlobalContext
{
static readonly Object LocatorLock = new object();
private static GlobalContext InternalInstance;
private GlobalContext() { }
public static GlobalContext Instance()
{
if (InternalInstance == null)
{
lock (LocatorLock)
{
if (InternalInstance == null)
{
InternalInstance = new GlobalContext();;
}
}
}
return InternalInstance;
}
#region IGlobalContext Members
public ITransFactory TransFactory { get; set; }
#endregion
}
It is worth noting that the TransFactory
property has a public
setter, we will address this issue when we cover dependency injection in a later chapter.
Request Context
In contrast to the global context, the request context is fully aware of the incoming requests. Its main role is to provide resources and services to individual requests ensuring that the request does not interfere with other requests' resources. Request resources are terminated when the request is serviced.
The BusinessNotifier
needs to store business warnings for the duration of the request, as multiple requests might process concurrently a BusinessNotifier
instance needs to be created for each request. The Request Context provides a single point for acquiring request resources like the BusinessNotifier
.
public interface IRequestContext
{
IBusinessNotifier Notifier { get; }
}
It is worth noting that where others assign an NHibernate session to the request when this one is created, our approach for this concern is going to be different. The unit of work solution presented in Chapter IV relays on the services creating a Transaction Manager using a factory class. Remember that it is the role of the factory to provide a RepositoryLocator
to the Transaction Manager. Services then delegate to the Transaction Manager to execute their commands, in this way commands are given the RepositoryLocator
that is required for accessing the back-end repositories. There are few pieces involved in this process but the design decouples responsibilities in a sleek manner so we achieve a very flexible design which is easily testable with its factory in combination with our services look after this aspect in a very slick manner.
For examples of the different approaches mentioned above, you may want to check the following links:
The Container
The container is just a helper wrapping the global and request contexts that helps locating request and global resources.
public class Container
{
private static Container InternalInstace;
private Container(){}
private static Container Instance()
{
if (InternalInstace != null) return InternalInstace;
InternalInstace = new Container();
return InternalInstace;
}
public static IGlobalContext GlobalContext
{
get
{
return AppServices.GlobalContext.Instance();
}
}
public static IRequestContext RequestContext { get; set; }
}
The GlobalContext
property does not need to expose a setter as we will always use the same implementation. However, we mentioned that we require different implementations of the IRequestContext
depending on if we are running in-memory or NHibernate mode. As mentioned above, we will resolve the public
setter in a later chapter.
The BusinessNotifier
Business warnings are just string
messages created to indicate the user of some incident or relevant situation that occurs during the business processing. The BusinessNotifier
provides a method to the services and domain entities for adding new messages and the Transaction Manager checks for warnings and adds them to the response so they can be processed on the client side.
public class BusinessNotifier
: IBusinessNotifier
{
private readonly IList<BusinessWarning> WarningList = new List<BusinessWarning>();
#region Implementation of IBusinessNotifier
...
public IEnumerable<BusinessWarning> RetrieveWarnings()
{
if (!HasWarnings) return null;
var results = WarningList.ToList();
WarningList.Clear();
return results;
}
#endregion
}
ServiceBase Re-factor
Now we can remove the Factory property in the ServiceBase
class and obtain the transaction manager instance using the new Container
class:
Before:
After:
Transaction Manager Changes
Now that the BusinessNotifier
is available, we can enhance the Transaction Manager implementing the CheckForWarnings
method that we left blank:
So the new implementation checks if warnings were added to the BusinessNotifier
the method, if so it adds them to the response instance:
New Test
We have added some new core functionality so it is a good idea to increase the test coverage. Let's modify the FindAll
service method in the CustomerService
so a business warning is created if customer instances are not found:
public class CustomerService
:ServiceBase, ICustomerService
{
...
public CustomerDtos FindAll()
{
return ExecuteCommand(locator => FindAllCommand(locator));
}
private CustomerDtos FindAllCommand(IRepositoryLocator locator)
{
var result = new CustomerDtos { Customers = new List<CustomerDto>() };
locator.FindAll<Customer>().ToList()
.ForEach(c => result.Customers.Add(Customer_to_Dto(c)));
if (result.Customers.Count() == 0)
{
Container.RequestContext.Notifier.AddWarning(
BusinessWarningEnum.Default, "No customer instances were found");
}
return result;
}
...
}
The new test then is:
[TestMethod]
public void CheckFindAllNotification()
{
var result = Service.FindAll();
Assert.IsTrue(result.Customers.Count == 0, "No customers were expected");
Assert.IsTrue(result.Response.HasWarning, "Warning flag is not set");
Assert.IsTrue(result.Response.BusinessWarnings.Count() == 1,
"One warning was only expected");
Assert.AreSame(result.Response.BusinessWarnings.Single().Message,
"No customer instances were found");
CreateCustomer();
result = Service.FindAll();
Assert.IsFalse(result.Response.HasWarning, "Warning flag is set");
}
Chapter Summary
With this chapter, we finish setting up the baseline of our server side components. We are now ready to start working in our rich client as we have established a comprehensive infrastructure on the server side for the client to work. We will see how the client can interact with our services and business domain classes without the need of having NHibernate nor WCF working. This approach aligns with Agile methodologies so we can start our business exploration and provide quick feedback to our customers without having the expensive back-end infrastructure in place.
The next chapter introduces the rich client and the basics of the MVVM using WPF. We will need another three or four chapters to define the basic infrastructure of the rich client.