Click here to Skip to main content
15,867,453 members
Articles / Web Development / ASP.NET

How to Build Flexible and Reusable WCF Services

Rate me:
Please Sign up or sign in to vote.
4.85/5 (13 votes)
6 May 2013GPL313 min read 76.9K   1.9K   126   7
Design Patterns and best practices for building flexible and reusable WCF services.

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.

C#
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.

C#
/// <summary>
/// A base class for all Xomega service interfaces
/// that provides common functionality for all interfaces.
/// </summary>
[ServiceContract]
public interface IServiceBase
{
    /// <summary>
    /// Validates and saves all changes that have been made during prior service calls in the same session.
    /// If there are any validation errors during saving of the changes than a fault will be raised
    /// with an error list that contains all the errors. A fault will also be raised if there are only
    /// validation warnings and the <c>suppressWarnings</c> flag is passed in as false. In this case
    /// the client can review the warnings and re-issue the service call with this flag set to true
    /// to proceed regardless of the warnings.
    /// </summary>
    /// <param name="suppressWarnings">True to save changes even if there are warnings,
    /// False to raise a fault if there are any warnings.</param>
    /// <returns>The number of objects that have been added,
    /// modified, or deleted in the current session.</returns>
    /// <seealso cref="System.Data.Objects.ObjectContext.SaveChanges()"/>
    [OperationContract]
    [FaultContract(typeof(ErrorList))]
    int SaveChanges(bool suppressWarnings);

    /// <summary>
    /// An explicit call to end the service session to support custom session mechanism for http bindings
    /// in Silverlight. This will allow releasing the instance of the service object on the server.
    /// </summary>
    [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.

C#
// Employee service that extends the base service class using the AdvWorksEntities
// object context, which is available as the objCtx member from the base class.
public class EmployeeService : EntityServiceBase<AdvWorksEntities>, IEmployeeService
{
    public EmployeeService()
    {
        // Initialize the resource manager for the errors,
        // so that you could use error codes and return localized error messages.
        ErrorList.ResourceManager = Resources.ResourceManager;
    }
}

Here is an example of how to implement a self-validating entity that reports localizable error or warning messages.

C#
// Complements the generated Empoloyee entity
public partial class Employee : IValidatable
{
    public void Validate(bool force)
    {
        // Validate employee's age and report an error using an error code,
        // which is also a key for a localized message from the resource file.
        // Examples of the resulting message are shown in the comments.
        if (BirthDate > DateTime.Today.AddYears(-18))
        {
            // Invalid new employee. Employee cannot be younger than 18 years old.
            // Invalid employee 123. Employee cannot be younger than 18 years old.
            ErrorList.Current.AddError("EMP_TOO_YOUNG", KeyParams());
        }

        // Validate employee's age and marital status and report a warning using
        // the key for a localized message from the resource file.
        // Examples of the resulting message are shown in the comments.
        if (BirthDate > DateTime.Today.AddYears(-20) && MaritalStatus != "S")
        {
            // Please confirm the marital status of the new employee, who is younger than 20 years old.
            // Please confirm the marital status of the employee 123, who is younger than 20 years old.
            ErrorList.Current.AddWarning("EMP_TOO_YOUNG_TO_MARRY", KeyParams());
        }
    }

    // Utility method to return employee key for existing employees
    // or a word 'new' for substitution into the error messages as parameters.
    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.

C#
// Employee service that extends the base service class using the AdvWorksEntities
// object context, which is available as the objCtx member from the base class.
public class EmployeeService : EntityServiceBase<AdvWorksEntities>, IEmployeeService
{
    public EmployeeService()
    {
        // Initialize the resource manager for the errors,
        // so that you could use error codes and get localized error messages.
        ErrorList.ResourceManager = Resources.ResourceManager;
    }

    public EmployeeKey Create()
    {
        Employee emp = new Employee();
        objCtx.AddToEmployee(emp);
        EmployeeKey res = new EmployeeKey();
        // create a temporary key to be returned
        res.TemporaryKey = TempKeyId(emp);
        return res;
    }

    public void Update(Employee_UpdateInput input)
    {
        // get the entity key by either temporary or real key
        EntityKey key = GetEntityKey(typeof(Employee), input.TemporaryKey, input.EmployeeId);
        // find the entity by entity key
        Employee emp = objCtx.GetObjectByKey(key) as Employee;
        // copy values from properties with the same names
        ServiceUtil.CopyProperties(input, emp);
        // find and set a reference to the manager employee
        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.

  1. Silverlight supports only a very limited set of bindings (mainly the BasicHttpBinding), which do not support sessions.
  2. Silverlight allows only asynchronous service invocation.
  3. 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.

C#
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.

XML
<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.

C#
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;
            }
            // end the session initiated by the ClientSessionManager.Register
            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);
        }

        // for new objects create the object and store a temporary key
        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. 

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)


Written By
Architect Xomega.Net
United States United States
Xomega Team is striving to increase productivity and development quality by utilizing Model Driven Development coupled with Code Generation and the best design practices for application development.
We provide MDD tools, code generators and frameworks for Visual Studio and .Net development.
Visit us at http://www.xomega.net
This is a Organisation

1 members

Comments and Discussions

 
QuestionNow Microservices ? Pin
kiquenet.com25-Mar-18 23:04
professionalkiquenet.com25-Mar-18 23:04 
QuestionXomega Pin
hajzerbela@gmail.com1-Apr-13 23:28
hajzerbela@gmail.com1-Apr-13 23:28 
AnswerRe: Xomega Pin
Xomega Team4-Apr-13 16:45
Xomega Team4-Apr-13 16:45 
GeneralMy vote of 5 Pin
NewSilence23-Jan-12 14:47
NewSilence23-Jan-12 14:47 
GeneralRe: My vote of 5 Pin
Xomega Team24-Jan-12 13:32
Xomega Team24-Jan-12 13:32 
GeneralMy vote of 5 Pin
Kanasz Robert20-Jan-12 1:54
professionalKanasz Robert20-Jan-12 1:54 
GeneralRe: My vote of 5 Pin
Xomega Team20-Jan-12 6:40
Xomega Team20-Jan-12 6:40 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.