Introduction
As you may know, designing and building a flexible and reusable service layer is essential when creating robust multi-tier
applications. In this article, we will discuss the pros and cons of different architectural approaches to building a service layer.
You will learn design patterns that help in building reusable services, and we will demonstrate how they
are implemented for WCF services with the Open Source Xomega Framework.
We will also talk about the challenges of using WCF in Silverlight as well as solutions to work around those challenges.
Architectural Overview
The first question you face before you start designing your service layer is whether your service layer will be inherently stateless or potentially stateful.
Pros and Cons of Stateless Services
With purely stateless services, each request is totally independent of each other, which has its own advantages, but doesn’t quite lend itself to building flexible and reusable
services as we explain next. The benefits of the stateless services are great scalability and low resource utilization, such as memory consumption and thread
usage. You can load-balance your services by running a cluster of servers and each server will be able to process any request independently without consuming
any memory to store the state, and then release all resources such as threads or database connections after the request has been processed.
The downside, however, is that you will have to pass the entire state along with each request. This may not be a problem if your state is always relatively small,
such as read/query operations or simple updates that are performed as one unit of work. In large systems and enterprise
applications though that require multiple levels of validations before the data can be saved, the user state may contain numerous edits that span multiple
objects. Designing a service that can take all the user edits in a single request can be a very challenging task in this case, and you will likely need
to design a separate service or operation for every different scenario, which will make your services not reusable and may eventually lead to a maintenance nightmare.
How Stateful Services Help with Flexibility and Reusability
With stateful services, on the other hand, the client may be able to issue a series of requests as part of the same session, which will
store all the changes on the server locally within that session until the client issues a dedicated request to validate the changes and save them in the
database. This approach allows you to define a small set of relatively simple reusable update operations, which you can call then in any possible combination
within the same session to implement different scenarios.
For example, your service may have an operation to update a certain entity, such as an employee, but also an operation to create new employees,
which may require the same input as with the update. Instead of repeating the input structures for both operations and duplicating the logic in their
implementations, it would make more sense for the create operation to simply create a blank new employee with a temporary key without saving it and then
return that temporary key back to the client. In this case, the client will pass the temporary key to the subsequent update operation in order to set the entity
values before saving it. We will show you how this design pattern can be implemented on the framework level in the following sections.
Pitfalls of Stateful Services
Even if you have implemented stateful services in your service layer, there may be different ways of how the client initiates and ends
the sessions, which may have serious architectural implications.
In one approach, the client may start the editing session as soon as the user starts making any edits, which can be as soon as the user
opens the form, and then finishes the session when the user hits Save. This is akin to opening a database transaction when the user starts editing and keeping
it open until the user saves the changes. While this may slightly simplify the programming logic on the client side, as the client doesn’t have to track any
changes at all, and can simply call the service operations directly in response to the user changes, this can also have a negative impact on the scalability
and reliability of your service layer. Since the user editing sessions may take a long time, the service layer will have to maintain the session in memory for
the whole duration of the session, and will have to route all the requests to the same service instance, which makes the application less scalable or
fault-tolerant. It will also present a problem of handling dangling sessions in case the user closes the form without saving it and the session isn’t properly terminated.
The Best of Both Worlds
A better solution would be for the client to not send any updates to the server until the user actually hits Save, at which point it will
open a session, issue all the necessary requests for the updates, and then finally a request to commit all the changes. This approach will get the best of
both worlds, since calling multiple operations within a single session still allows designing service operations in a flexible and reusable way, and yet the
fact that they are all called at the same time minimizes the impact on scalability and performance, which would be almost as good as those for stateless services.
Sessions in WCF
The most straightforward way to support stateful services that we described above in WCF is to configure it so that the client could
start a session, which will create a dedicated instance of a service implementation object that will handle all client requests within the same session and in the
same order that they are sent by the client. As the last request, the client will send an explicit command to save all the changes, which will validate the
changes and commit them to the database. The client will then close the session, which will destroy the dedicated service instance and release all associated resources.
WCF provides good support for sessions like that, although not all WCF bindings support that. For example, NetTcpBinding
and WSHttpBinding
both support sessions, while BasicHttpBinding
doesn’t support it. It also provides a number of attributes that allow controlling the session behavior as follows.
SessionMode
is a service contract attribute that lets you mandate or prevent using the sessions on that service, or just allow using
the session if the current binding supports it, which is the default behavior that we recommend keeping.InstanceContext
is a service implementation class attribute that specifies how you want to instantiate the service objects: per call, per session or as a global singleton.ConcurrencyMode
is a service object attribute that indicates whether or not a single instance can handle multiple requests at the same time.
We recommend keeping the default setting, which makes it essentially single-threaded.OperationContract
is an operation level attribute on the service interface that among everything else allows indicating if the operation
always initiates a new session or terminates the previous session or both. The default behavior is none of those, which is what we recommend,
so that the client could explicitly control when the session starts and completes.
To recap, in order to implement the stateful services that we described, you need to use a WCF binding that supports sessions and just
keep the default instancing behavior, which will create a new session when the client opens a new channel with the server, will use that session for any
communication over that channel and will finally terminate the session when the channel is closed. The following snippet demonstrates the corresponding client code.
private void btnSave_Click(object sender, RoutedEventArgs e)
{
ChannelFactory<IEmployeeService> cfEmployee =
new ChannelFactory<IEmployeeService>("IEmployeeService");
IEmployeeService svcEmployee = cfEmployee.CreateChannel();
Employee_UpdateInput inUpdate = new Employee_UpdateInput();
obj.ToDataContract(inUpdate);
svcEmployee.Update(inUpdate);
svcEmployee.SaveChanges(true);
cfEmployee.Close();
}
Implementation of Services Design Patterns
To demonstrate the implementation of the design patterns for building flexible and reusable services we will use the Entity Framework to
allow making changes to the data and then validating and saving it separately, as this is what our open source Xomega Framework currently supports.
Validation and Save
The framework defines a base interface that any service can inherit from, which has a common operation for validating and saving all the changes in the session as follows.
[ServiceContract]
public interface IServiceBase
{
[OperationContract]
[FaultContract(typeof(ErrorList))]
int SaveChanges(bool suppressWarnings);
[OperationContract]
void EndSession();
}
This operation allows you to validate all the changes and report any validation error messages back to the client by severity. If a
critical error is encountered during validation, e.g. if a certain field is blank, and this prevents further validation, then the process will stop and
report the errors immediately. Otherwise the validation will execute completely, and if this yields any errors, then those will be reported to the
client and the save will not succeed. If however the validation will result in only warnings but no errors, then the server will either report them to the
client without saving or go ahead and save the changes depending on the value of the suppressWarnings
flag that was passed in. This allows the client to call
this operation twice – first without suppressing the warnings to show any warnings to the users and give them a chance to correct the data and resubmit,
and second time actually suppressing the warnings, if the users chose to ignore them.
The framework also defines a template base class for all service implementations to extend from, which uses the Entity Framework’s object
context as a template parameter. The base service implements the SaveChanges
operation with the behavior we described, and validates all modified entities in the
current object context that implement IValidatable
interface of the Xomega Framework. To implement the actual validation you can add a partial class for
any of your entity classes and make them implement IValidatable
.
The following code snippet demonstrates how a concrete service implementation can subclass the Xomega Framework base service class.
public class EmployeeService : EntityServiceBase<AdvWorksEntities>, IEmployeeService
{
public EmployeeService()
{
ErrorList.ResourceManager = Resources.ResourceManager;
}
}
Here is an example of how to implement a self-validating entity that reports localizable error or warning messages.
public partial class Employee : IValidatable
{
public void Validate(bool force)
{
if (BirthDate > DateTime.Today.AddYears(-18))
{
ErrorList.Current.AddError("EMP_TOO_YOUNG", KeyParams());
}
if (BirthDate > DateTime.Today.AddYears(-20) && MaritalStatus != "S")
{
ErrorList.Current.AddWarning("EMP_TOO_YOUNG_TO_MARRY", KeyParams());
}
}
public object[] KeyParams()
{
if (EntityKey.IsTemporary) return new string[] { Resources.NEW, "" };
else return new string[] { "", " " + EmployeeId };
}
}
Entity Creation
Another design pattern for building flexible and reusable services revolves around the ability to create blank entities and then use the
same update service operations to set entity values as those used for editing existing entities. The easiest way to do it is to make the create operation
return a temporary key for the newly created entity and then have the update operations accept either a temporary key or a regular key.
Entity Framework supports a similar concept on its own, except that temporary entity keys cannot be serialized and sent back and forth
between the client and the service, since the same temporary entity keys should actually reference the same EntityKey
object in memory.
Xomega Framework solves this issue in the base service implementation class by storing temporary entity keys in a hash table using
their hash code as a hash key. This way the create operation can return the integer hash key as a temporary key back to the client, which it can obtain by
calling the TempKeyId
method of the base service with a temporary EntityKey
.
The update operations will then accept both temporary key and real key values and will call the GetEntityKey
method of the base service
to obtain the actual EntityKey
, which it can further use to find the entity in the object context. The following example illustrates this approach
in a service implementation class.
public class EmployeeService : EntityServiceBase<AdvWorksEntities>, IEmployeeService
{
public EmployeeService()
{
ErrorList.ResourceManager = Resources.ResourceManager;
}
public EmployeeKey Create()
{
Employee emp = new Employee();
objCtx.AddToEmployee(emp);
EmployeeKey res = new EmployeeKey();
res.TemporaryKey = TempKeyId(emp);
return res;
}
public void Update(Employee_UpdateInput input)
{
EntityKey key = GetEntityKey(typeof(Employee), input.TemporaryKey, input.EmployeeId);
Employee emp = objCtx.GetObjectByKey(key) as Employee;
ServiceUtil.CopyProperties(input, emp);
if (input.Manager.HasValue)
{
EntityKey mgrKey = GetEntityKey(typeof(Employee), input.Manager.Value);
emp.ManagerObject = objCtx.GetObjectByKey(mgrKey) as Employee;
}
emp.ModifiedDate = DateTime.Now;
}
}
Silverlight Challenges
If you try to call WCF services from a Silverlight application, you will most likely face certain challenges due to the following Silverlight limitations.
- Silverlight supports only a very limited set of bindings (mainly the
BasicHttpBinding
), which do not support sessions. - Silverlight allows only asynchronous service invocation.
- WCF services for Silverlight should be hosted within the same web application as the Silverlight application itself.
The first Silverlight limitation of not supporting sessions is the most challenging to work around. Xomega Framework provides an easy-to-use
solution to this by letting you register your Silverlight client channel with the Xomega client session manager, which will send the session information for
your channel in the HTTP header. There is no need to unregister it, since it will be done automatically when the channel is closed. Below is a code snippet that illustrates this.
EmployeeServiceClient cltEmployee = new EmployeeServiceClient();
ClientSessionManager.Register(cltEmployee.InnerChannel);
On the server side though, you want to make sure that requests from the same session get routed to the same instance of the service.
In order to do that Xomega Framework provides a special instance behavior, which you can easily configure as part of your WCF service model configuration as follows.
<system.serviceModel>
<behaviors>
<endpointBehaviors>
<behavior name="BasicInstanceBehavior">
<HttpSessionInstanceBehavior/>
</behavior>
</endpointBehaviors>
</behaviors>
<services>
<service name="AdventureWorks.Services.EmployeeService">
<endpoint address="" binding="basicHttpBinding"
contract="AdventureWorks.Services.IEmployeeService"
behaviorConfiguration="BasicInstanceBehavior">
</endpoint>
</service>
</services>
<extensions>
<behaviorExtensions>
<add name="HttpSessionInstanceBehavior"
type="Xomega.Framework.Services.HttpSessionInstanceBehavior,
Xomega.Framework, Version=1.2.0.0, Culture=neutral,
PublicKeyToken=null"/>
</behaviorExtensions>
</extensions>
</system.serviceModel>
The second Silverlight limitation that requires asynchronous WCF calls only means that in order to implement a conversation, where a service call needs a result from the
previous service call, you will need to chain each subsequent service call in a separate callback function that you pass as part of the previous service call.
To leverage the Xomega session support here you need to make sure that you use the same instance of the channel for each such call. This is pretty straightforward if
you inline your callback functions all within the same method, which will have visibility to the channel declared at the beginning of the method. However, you
want to specifically take care of it if you reuse some of the callback functions.
For example, you may have a method that reads an entity that you use to display existing entities. However, if the user creates a new entity
and saves it, then you may want to call the same read method to retrieve the permanent key instead of the temporary key and refresh any other fields that
may have been changed during the save. In this case the read operation should happen in the same session as the corresponding create and save or your
temporary key wouldn’t be recognized. You’ll need to pass your channel to the read method for that. The following code demonstrates this scenario.
public partial class EmployeeObjectPage : Page
{
private EmployeeObject obj;
private bool isNew = false;
public EmployeeObjectPage()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
obj = new EmployeeObject();
pnlMain.DataContext = obj;
isNew = NavigationContext.QueryString.Count == 0;
if (!isNew) Load(null);
}
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
bool? modified = obj.IsModified();
if (modified.HasValue && modified.Value && MessageBox.Show(
"You have unsaved changes. Do you want to discard them and navigate away?",
"Unsaved Changes", MessageBoxButton.OKCancel) == MessageBoxResult.Cancel)
e.Cancel = true;
}
private void Load(EmployeeServiceClient client)
{
if (client == null)
{
int kEmployeeId;
if (!int.TryParse(NavigationContext.QueryString["EmployeeId"], out kEmployeeId)) return;
obj.EmployeeIdProperty.Value = kEmployeeId;
}
EmployeeServiceClient cltEmployee = client ?? new EmployeeServiceClient();
cltEmployee.ReadCompleted += delegate(object sender, ReadCompletedEventArgs e)
{
FaultException<ErrorList> fex = e.Error as FaultException<ErrorList>;
if (fex != null && fex.Detail != null)
MessageBox.Show(fex.Detail.ErrorsText, "Service Errors", MessageBoxButton.OK);
else
{
obj.FromDataContract(e.Result);
isNew = false;
btnDelete.IsEnabled = true;
}
if (client != null) cltEmployee.EndSessionAsync();
cltEmployee.CloseAsync();
};
EmployeeKey inRead = new EmployeeKey();
obj.ToDataContract(inRead);
cltEmployee.ReadAsync(inRead);
}
private void btnSave_Click(object sender, RoutedEventArgs e)
{
obj.Validate(true);
ErrorList valErr = obj.GetValidationErrors();
if (valErr.HasErrors())
{
MessageBox.Show(valErr.ErrorsText, "Validation Errors", MessageBoxButton.OK);
return;
}
EmployeeServiceClient cltEmployee = new EmployeeServiceClient();
ClientSessionManager.Register(cltEmployee.InnerChannel);
cltEmployee.SaveChangesCompleted += delegate(object s, SaveChangesCompletedEventArgs args)
{
FaultException<ErrorList> fex = args.Error as FaultException<ErrorList>;
if (fex != null && fex.Detail != null)
MessageBox.Show(fex.Detail.ErrorsText, "Service Errors", MessageBoxButton.OK);
else
{
obj.SetModified(false, true);
Load(cltEmployee);
}
};
cltEmployee.UpdateCompleted += delegate(object s, AsyncCompletedEventArgs args)
{
cltEmployee.SaveChangesAsync(true);
};
if (!isNew)
{
Employee_UpdateInput inUpdate = new Employee_UpdateInput();
obj.ToDataContract(inUpdate);
cltEmployee.UpdateAsync(inUpdate);
}
cltEmployee.CreateCompleted += delegate(object s, CreateCompletedEventArgs args)
{
obj.FromDataContract(args.Result);
Employee_UpdateInput inUpdate = new Employee_UpdateInput();
obj.ToDataContract(inUpdate);
cltEmployee.UpdateAsync(inUpdate);
};
if (isNew) cltEmployee.CreateAsync();
}
private void btnDelete_Click(object sender, RoutedEventArgs e)
{
if (MessageBox.Show(
"Are you sure you want to delete this object?\nThis action cannot be undone.",
"Delete Confirmation", MessageBoxButton.OKCancel) == MessageBoxResult.Cancel) return;
if (isNew) Close();
EmployeeServiceClient cltEmployee = new EmployeeServiceClient();
ClientSessionManager.Register(cltEmployee.InnerChannel);
cltEmployee.SaveChangesCompleted += delegate(object s, SaveChangesCompletedEventArgs args)
{
FaultException<ErrorList> fex = args.Error as FaultException<ErrorList>;
if (fex != null && fex.Detail != null)
MessageBox.Show(fex.Detail.ErrorsText, "Service Errors", MessageBoxButton.OK);
else
{
obj.SetModified(false, true);
cltEmployee.CloseAsync();
Close();
}
};
cltEmployee.DeleteCompleted += delegate(object s, AsyncCompletedEventArgs args)
{
cltEmployee.SaveChangesAsync(true);
};
EmployeeKey inDelete = new EmployeeKey();
obj.ToDataContract(inDelete);
cltEmployee.DeleteAsync(inDelete);
}
private void btnClose_Click(object sender, RoutedEventArgs e)
{
Close();
}
private void Close()
{
NavigationService.GoBack();
}
}
As for the third limitation, the easiest solution is to use the same ASP.NET web application to host both your Silverlight application and the
WCF services. If this is not an option, e.g. because your WCF services are hosted separately, then you need to configure a cross-domain policy for your
services. This article provides a good overview for that.
See This Approach in Action
If you would like to try out these design patterns for building flexible and reusable WCF services, you can download our open source Xomega
Framework from CodePlex or install it as a package using NuGet package manager. It also includes a powerful and innovative UI framework as well as
support for best practice design patterns, which provides an extremely easy and fast way to build powerful enterprise level multi-tier WPF, Silverlight, or
ASP.NET applications with a WCF service layer and Entity Framework-based business logic layer.
We value your feedback, so please leave a comment if you like the article, have any questions or would like to share your experience with
building a service layer or with using our framework.
Additional Resources
To learn about the powerful UI framework functionality of the Xomega Framework, please read our article: Take MVC To The Next
Level in .Net.
History
- 1/20/2012 - The first version of the article published.
- 1/24/2012 - AdventureWorks-based solution with examples uploaded.