WCF RIA Services Toolkit: SOAP Endpoints

If you have used WCF RIA Services along with Entity Framework for Silverlight applications, you know how useful they are for performing database operations from your application.  With the WCF RIA Services Toolkit, it gives you even more flexibility to expose your RIA DomainService methods to not only a Silverlight application, but any application that can access a SOAP or JSON endpoint.
I will provide some steps here for setting up and consuming data through a SOAP endpoint.

1. Install WCF RIA Services Toolkit

You will need to install the toolkit in your development environment.  You can get it here.

2.  Web.config update

In order to have your server expose a SOAP endpoint, we need to add a new section in the web.config for the project that contains your DomainService class (you may already have these sections defined, so you may only need to add the "endpoints" section):

<configuration>
  <configSections>
    <sectionGroup name="system.serviceModel">
      <section name="domainServices" type="System.ServiceModel.DomainServices.Hosting.DomainServicesSection, System.ServiceModel.DomainServices.Hosting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" allowDefinition="MachineToApplication" requirePermission="false" />
    </sectionGroup>
  </configSections>
  <system.serviceModel>
    <domainServices>
      <endpoints>
        <add name="soap" type="Microsoft.ServiceModel.DomainServices.Hosting.SoapXmlEndpointFactory, Microsoft.ServiceModel.DomainServices.Hosting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
      </endpoints>
    </domainServices>
  </system.serviceModel>
</configuration>


3. Add Service Reference to SOAP Endpoint

Now that we have our "server" end configured, we can set up our "client" project that will use the SOAP endpoint.  In your client, project, right-click on the "References" or "Service References" folder and select "Add Service Reference..."

Since RIA Service dynamically generates the endpoints, we need to construct our own URL for the service address.  It helps if you host your application in IIS so you don't need to worry about changing ports, but either way, use the namespace and class name of your DomainService on the server to construct your URL.  You can test the URL in a browser to make sure you get the service info page. 

http://localhost/Application-Name/Namespace-Of-DomainService-DomainServiceClassName.svc

The default values can remain for the other Service Reference Settings.  After the Service Reference is added, Visual Studio will build proxy classes to call this service.

NOTE: If you add the Service Reference in a project that is not a web project (i.e. a class library), be sure to copy the generated service endpoints and bindings from your App.config to the hosting site's web.config.

4. Getting Data from the RIA DomainService

Now that the service reference is added, we can start writing code to use the RIA DomainService methods.

Let's say we have a DomainService method named GetAuthors() that gets a collection of Author objects from the database (you can ignore the Include part for now):

[Query]
public IQueryable<Author> GetAuthors()
{
    return this.ObjectContext.Authors.Include("Books");
}

And then in your "client" project, you can add code to use the service to fetch the collection of Authors:

// Initialize the SoapClient (consider making this a static class member)
var _domainServiceClient = new MyDomainServiceSoapClient();
 
var results = _domainServiceClient.GetAuthors();
// authors variable will be a collection of Author objects from the server
var authors = results.RootResults;

In the previous GetAuthors() query method, I had .Include("Books") at the end.  This allows us to load child objects through an Entity Framework navigation property.  As long as you have foreign keys defined in your database, Entity Framework will load these as navigation properties that can be included with the parent results.  However, when you do this, you will notice that the RootResults of the service does not contain any child objects.

To get the child collections, we need to use the IncludedResults property of the QueryResult return object.  In this example, I will load the books for the author into a separate collection (you could also built a partial class of the Author object that adds an IEnumerable<Book> Books property that could be populated).

// Initialize the SoapClient (consider making this a static class member)
var _domainServiceClient = new MyDomainServiceSoapClient();
 
var results = _domainServiceClient.GetAuthors();
// authors variable will be a collection of Author objects from the server
var authors = results.RootResults;
// books will be a collection of Book objects related to the Author
// the IncludedResults may contain different objects, so filter and cast here
var books = results.IncludedResults.Where(included => included is Book).Cast<Book>();


5. Saving Changes

When you use WCF RIA Services with Silverlight, things are especially easy because there is a local context object in Silverlight that holds all of your objects and automatically does change tracking for you.  All you have to do is call SubmitChanges() to persist any changes back to the database.

When we use the SOAP endpoint, the work of submitting the changes is the same, but we need to keep track of WHAT has changed ourselves.  When you add your service reference, Visual Studio adds a SubmitChanges() method for you that expects an array of ChangeSetEntry objects.  I built a single method that will submit changes for anything in our client application by taking in that array of ChangeSetEntries:

/// <summary>
/// Global method used to save ChangeSets to the DomainService
/// </summary>
/// <param name="changeSetEntries">Array of ChangeSetEntry objects</param>
/// <returns>True if successful, False if errors occurred</returns>
private static bool SubmitChanges(ChangeSetEntry[] changeSetEntries)
{
    try
    {
        var result = _domainServiceClient.SubmitChanges(changeSetEntries);
 
        var errors = result.Where(r => r.ValidationErrors != null).SelectMany(s => s.ValidationErrors).ToList();
 
        if (errors.Any())
        {
            _log.Warn(string.Join("; ", errors.Select(e => e.Message)));
            return false;
        }
    }
    catch (Exception ex)
    {
        _log.Error("Error saving data", ex);
        return false;
    }
    return true;
}

Now the code to call SubmitChanges().  Inserting and Deleting objects are the easier operations to build.  All you need to supply in the ChangeSetEntry is the Entity and the Operation:

var changeSet = new ChangeSetEntry
    {
        Entity = author,
        Operation = DomainOperation.Insert
    };
 
return SubmitChanges(new[] { changeSet });

Update operations require a little more work.  If you are familiar with RIA Services, you know that it expects an OriginalEntity for update operations so it can detect concurrency issues.  Therefore, to do an update, we need to supply the original Author values when updating.

One way to do this is to create a partial Author class and add an OriginalAuthor property that can store our original value.  Since this is created on the "client" side of the service, it is not included when it is sent back to the server to do the update.

/// <summary>
/// Extend the Author class to store original values
/// </summary>
public partial class Author
{
    public Author OriginalAuthor { get; set; }
}

To use this, populate the OriginalAuthor property immediately after fetching it from the service:

var results = _domainServiceClient.GetAuthors();
// authors variable will be a collection of Author objects from the server
var authors = results.RootResults;
// set original value for update
authors.ToList().ForEach(a => a.OriginalAuthor = a);

And now updating is not so difficult since we already have our original value:

var changeSet = new ChangeSetEntry
    {
        Entity = author,
        OriginalEntity = author.OriginalAuthor,
        Operation = DomainOperation.Update
    };
 
return SubmitChanges(new[] { changeSet });

Now you have the framework for an application that can use a single DomainService that can handle CRUD operations for a Silverlight application as well as any number of other applications through SOAP or JSON.
comments powered by Disqus