Share via


Data Services

Create Data-Centric Web Applications With Silverlight 2

Shawn Wildermuth

Code download available at:Code Gallery

This article was written using .NET 3.5 SP1 Beta and Silverlight 2 Beta 2. Now that .NET 3.5 SP1 is out of Beta, there are some incompatibilities between the released version of .NET 3.5 SP1 and Silverlight 2 Beta 2. The code in this article may not work until Silverlight 2 has been updated to work with .NET 3.5 SP1. This code will be updated at that time.

This article discusses:

  • Connecting Silverlight to a data source
  • Using ADO.NET Data Services
  • Using LINQ to SQL
  • Debugging the service
This article uses the following technologies:
Silverlight 2 Beta 2, LINQ, ADO.NET Data Services

Contents

ADO.NET Data Services
Creating the Service
Querying and Updating Data
The Silverlight 2.0 Client Library
Loading Related Entities
Change Management
Updating through the Service
Debugging the Service
Where We Are

Line-of-business and other data-centric applications can be built in Silverlight™, but working with data in Silverlight isn't always simple. While Silverlight contains many of the tools you need in order to consume data and support Web services and XML, these tools only represent the basics of data access across a firewall.

A common data-access strategy uses a mix of Web services and client-side LINQ, a highly recommended approach if you're leveraging existing Web service endpoints to power your Silverlight applications. But if you are building new Web services to work specifically with Silverlight, there is an unnecessary cost involved.

For a typical Web service layer, you implement a traditional data-­access strategy on the server (custom business objects, LINQ to SQL, Entity Framework, NHibernate, and so on) and expose data objects through the Web service. The Web service then is merely a gateway to the underlying data-access strategy.

But to enable full data connectivity, you must map the four data operations (Create, Read, Update, and Delete) to Web service methods. For example, a simple service contract to support a Product class would look something like Figure 1 . (Note that I use C# throughout the article, but the code download includes Visual Basic ® code as well.)

Figure 1 Service Contract for Simple Product Web Service

[ServiceContract]
public interface ICustomerService
{
  [OperationContract]
  List<Product> GetAllProducts();

  [OperationContract]
  Product GetProduct(int productID);

  [OperationContract]
  List<Product> GetAllProductsWithCategories();

  [OperationContract]
  Product SaveProduct(Product productToSave);

  [OperationContract]
  void DeleteProduct(Product productToDelete);
}

It can be quite time-consuming to create a set of services to work with an application's entire data model. And, as this example shows, feature-specific operations can cause Web service bloat. In other words, over time a Web service will have new requirements and operations added, including operations that don't really belong to the core business domain.

For instance, in Figure 1 you see the operation GetAllProducts­WithCategories, which is used to retrieve the Product and include the categories by default. It wouldn't be surprising to see sorting, filtering, and even paging mechanisms added to this simple example. It would be nice if there were a simple way to support data operations (like querying, sorting, filter, and so on) without having to continually build all these mechanisms manually. That is where ADO.NET Data Services comes in.

ADO.NET Data Services

The goal of ADO.NET Data Services is to provide Web-accessible endpoints for a data model. These endpoints offer a way to filter, sort, shape, and page data from the server without requiring the developer to custom build the functionality for these operations. Essentially, each endpoint is a starting point for a LINQ query. From the endpoint, you can query for the data you are actually looking for.

But don't think of ADO.NET Data Services as just another data-access strategy. ADO.NET Data Services does not perform any data access directly. In fact, it's layered on top of a data-access technology that supports serialization, querying, and updating. The job of querying and updating the data is left to this underlying data-access layer. Figure 2 shows ADO.NET Data Services and its placement in a typical application architecture.

fig02.gif

Figure 2 ADO.NET Data Services Layers (Click the image for a larger view)

Since ADO.NET Data Services relies on a data-access facility to do the actual data-access work, you must have a way to specify what this work looks like. In ADO.NET Data Services, each service must be backed by a LINQ-enabled provider. In fact, each endpoint is really just an IQueryable endpoint. Thus, ADO.NET Data Services can support any object that supports IQueryable.

Creating the Service

When you add ADO.NET Data Services to your project, a new .svc file is created, as is a class that represents the service. Unlike a Web service, you will not actually implement the service operations yourself, but instead will allow the DataService class to handle most of the heavy lifting. To get the service running, there are a couple of small tasks that are necessary. First, the DataService class requires a type parameter called the context object. The context object is a class that describes the data to be exposed as a service. When your service exposes data from a relational database, this class is typically derived from Entity­Framework's ObjectContext or LINQ to SQL's DataContext:

// Use my NorthwindEntities context object 
// as the source of the Service's Data
public class Products : DataService<NorthwindEntities>

There is no base class requirement for the context object. In fact, you can create your own as long as its properties implement the IQueryable interface; ADO.NET Data Services will expose these properties as endpoints:

public class StateContext
{
  StateList _states = new StateList();
  public IQueryable<State> States 
  {
    get { return _states.AsQueryable<State>(); } 
  }
}

In the InitializeService call, you are handed an IDataServiceConfiguration object that you can use to specify what kinds of permissions to allow in the service. ADO.NET Data Services uses the abstraction of nouns and verbs for specifying permissions, as illustrated in Figure 3 .

Figure 3 Set Access Rules

// This method is called only once to initialize service-wide policies.
public static void InitializeService(IDataServiceConfiguration config)
{
  // Only allow us to read or update Products Entities
  // not Delete or Create
  config.SetEntitySetAccessRule("Products", 
                                EntitySetRights.AllRead | 
                                EntitySetRights.WriteUpdate);

  // Only Allow Reading of Category and Supplier Entities
  config.SetEntitySetAccessRule("Categories", EntitySetRights.AllRead);
  config.SetEntitySetAccessRule("Suppliers", EntitySetRights.AllRead);

}

Once this is done, you can browse to the service directly, and it will show the Atom feed information about each of the endpoints. For debugging of ADO.NET Data Services, I suggest you disable RSS feed view in Internet Explorer ® or use another browser to see the XML format of the service.

Querying and Updating Data

ADO.NET Data Services exposes the service as a Representational State Transfer (REST)-based service—it is not a SOAP-based service. This means that instead of SOAP envelopes, the payload of the response from the service contains only the data, not the request metadata. All requests are described using the combination of the HTTP verb (GET, PUT, POST, and so forth) and the request URI. Assume you have a model that describes Products, Categories, and Suppliers, as in Figure 4 . The ADO.NET Data Services service that results could have three endpoints, one for each entity set in the model. The URI to address an entity set in the model is simply the address of the service and the name of the endpoint: https://localhost/{ServiceName}/{EndPointName} or https://localhost/Product.svc/Products.

fig04.gif

Figure 4 Sample Data Model (Click the image for a larger view)

This URI syntax supports a host of different features including the retrieval of specific entities, sorting, filtering, paging, and shaping of the results.

ADO.NET Data Services uses these URL-style queries to return the data to the consumer of the service. Currently, two serialization formats are supported (though this is likely to expand in future versions): JavaScript Object Notation (JSON) and Atom-based XML. JSON is a format that is easy for client-side Web code to consume, whereas Atom is an XML-based format and subsequently requires the assistance of an XML parser.

Instead of requiring that the serialization format be specified in the query, ADO.NET Data Services uses standard HTTP accept headers to determine which format to return to the client. If you issue a request from a client (such as a browser) that can consume XML, and if you don't specify a preferred format type via an Accept header, Atom is the default format of the data returned.

Querying data is only part of the solution. Ultimately the solution must support both queries and updates. To support all of these requirements, ADO.NET Data Services maps the four basic data-access operations to the four basic HTTP verbs (see Figure 5 ).

Figure 5 Data-Access Verbs to HTTP Verbs

Data-Access Verb HTTP Verb
Create POST
Read GET
Update PUT
Delete DELETE

By using these verbs, ADO.NET Data Services lets the consumer of the service take advantage of all types of data operations without having to create special endpoints for the different types. The only requirement for getting data updating to work within ADO.NET Data Services is for the underlying data-access technology to support the IUpdatable interface. This interface is the contract defining how updates are propagated from ADO.NET Data Services to the data sources.

Currently, the Entity Framework is the only data-access technology that supports this interface. In the future, I would expect that most layers will also incorporate support for it (including LINQ to SQL, LLBGenPro, and NHibernate). Now that you have a working knowledge of ADO.NET Data Services, you can begin to use it in Silverlight 2.

The Silverlight 2.0 Client Library

If you were to use ADO.NET Data Services to issue queries and update data through the URI syntax and manipulate the XML directly, you would get the functionality you want, but you would still be building a lot of plumbing. That is where the ADO.NET Data Services Client Library for Silverlight comes in. This library (in conjunction with Visual Studio 2008) allows you to issue LINQ queries directly in your Silverlight applications, which are translated by the client library into HTTP query or update requests to the data service.

Before you can get started, you need to generate some code. This code generation reads the metadata of the ADO.NET Data Services service and generates data-only classes for the service's entities as well as a class to represent the service itself.

To generate this code, you will need to add a Service Reference to your project. If you are familiar with Service References as a way to add a reference to a Web service, it’s the same tool. Otherwise, you can right-click your Silverlight project in the Project Explorer and pick “Add Service Reference…”. In the dialog window, click the “Discover” button to show the services in your project (including ADO.NET Data Services). Pick the ADO.NET Data Service endpoint and press “OK” to dismiss the dialog. This will build a new file with a data contract class for each of the endpoints and a DataServiceContext-derived class. The DataServiceContext class is used as the service entry point (and exposes queryable service endpoints). This will include these classes in your Silverlight project and add a reference to the System.Data.Services.Client.dll (part of the Silverlight 2 SDK), and you will have all the code you need to work with ADO.NET Data Services.

This will build a new file with a data contract class for each of the endpoints and a DataServiceContext-derived class. The DataService­Context class is used as the service entry point (and exposes queryable service endpoints). If you include this class in your Silverlight project and add a reference to the System.Data.Services.Client.dll (part of the Silverlight 2 Beta 2 SDK), you will have all the code you need to work with ADO.NET Data Services.

The Silverlight client code is similar to other LINQ-based queries you might have written in .NET-targeted code. You create a Data­ServiceContext-derived class and issue LINQ queries against it. Here's how it would look:

// Create the Service class specifying the 
// location of the ADO.NET Data Services 
NorthwindEntities ctx = 
  new NorthwindEntities(new Uri("Products.svc", UriKind.Relative));

// Create a LINQ Query to be issued to the service
var qry = from p in ctx.Products
               orderby p.ProductName
                select p;

When you execute this query, it will issue a Web request to get the desired data. However, the Silverlight code here differs dramatically from standard LINQ queries in that Silverlight does not allow synchronous Web requests. Therefore, to make the execution asynchronous you need to first cast the query into a DataServiceQuery<T> object and then explicitly call Begin­Execute to start the asynchronous execution, as seen here:

// Cast to a DataServiceQuery<Product> 
// (since the query is returning Products)
DataServiceQuery<Product> productQuery =
  (DataServiceQuery<Product>)qry;

  // Execute the Query Asynchronously specifying 
  // a callback method
  productQuery.BeginExecute(new 
    AsyncCallback(OnLoadComplete),
    productQuery);

Once the query has executed (whether the operation was successful or not), the method specified in the AsyncCallback will be executed. This is the point at which you can enumerate the LINQ query to process the actual results. Typically you would include the original query in the AsyncCallback so you could retrieve it in the callback method (or you could save it as part of the class), as you can see in Figure 6 .

Figure 6 Add Results to Collection

void OnLoadComplete(IAsyncResult result)
{
  // Get a reference to the Query
  DataServiceQuery<Product> productQuery =
    (DataServiceQuery<Product>)result.AsyncState;

  try
  {
    // Get the results and add them to the collection
    List<Product> products = productQuery.EndExecute(result).ToList();

  }
  catch (Exception ex)
  {
    if (HtmlPage.IsEnabled)
    {
      HtmlPage.Window.Alert("Failed to retrieve data: " + ex.ToString());
    }
  }

}

If you have never dealt with LINQ before, this pattern will be quite foreign to you. At the current time, there are no good patterns for asynchronous LINQ except for executing LINQ in an asynchronous package (such as ThreadPool and BackgroundWorker). Silverlight requires all requests to be asynchronous, so this pattern is required when using the ADO.NET Data Services Client Library.

Loading Related Entities

ADO.NET Data Services also allows you to choose how you want to go about loading related entities. In the earlier example I gave, I was loading products from the server. Each product has a relationship to the supplier for that product as well as the category for that product.

Using the previous LINQ query, I only retrieved products. If I wanted to show supplier or category information, it would not have been immediately accessible. I would have either had to load this information as I needed it or explicitly retrieve it during my original query to the server. Both techniques have their benefits, but, in general, explicit loading is much more efficient if you know that you will need the information for every object. If you only expect to load that data for a few entities, then using on-demand retrieval is preferable.

By default, when there is a relationship property (such as Product.Supplier) that property is null if you don't explicitly load the property. To facilitate loading on demand, the DataService­Context class has a BeginLoadProperty method (that follows the same asynchronous model) where you specify the source entity, the name of the property, and a callback:

public void LoadSupplierAsync(Product theProduct)
{
  TheContext.BeginLoadProperty(theProduct, 
                               "Supplier", 
                               new AsyncCallback(SupplierLoadComplete), 
                               null);
  }

  public void SupplierLoadComplete(IAsyncResult result)
  {
    TheContext.EndLoadProperty(result);
  }

Once the EndLoadProperty is called, the property has been properly loaded with the related entity. In many cases, you will want to load them explicitly in the original query. To facilitate this, the LINQ provider supports the Expand extension method. This method allows you to specify the name of the property paths to load when the query executes. The Expand extension method is used in the From clause of the LINQ query to tell the provider to attempt to load those related entities. For example, if you change the original query to use Expand methods for both Category and Supplier, our objects will load those related entities during the original query execution:

var qry = 
  from p in TheContext.Products.Expand("Supplier").Expand("Category")
          orderby p.ProductName
          select p; 

If you are just reading data from ADO.NET Data Services, you are done. Knowing how to create a query, run it, and load related entities is all you need. If you need to actually modify data, there is a bit more work involved. Bind your new data to your Silverlight controls and go to town!

Change Management

The ADO.NET Data Services Client Library does not support automatic change monitoring of objects. This means that as objects, collections, and relationships change, it is up to the developer to tell the Data­ServiceContext object about those changes. The API for notifying the DataServiceContext object is fairly simple and is illustrated in Figure 7 .

Figure 7 DataServiceContext Change API

Method Description
AddObject Adds a newly created object.
UpdateObject Marks an object as changed.
DeleteObject Marks the object for deletion.
AddLink Adds a link between two objects.
UpdateLink Updates a link between two objects.
DeleteLink Deletes a link between two objects.

This means that you will need to monitor changes to the objects and notify the DataServiceContext object with your own code. On the surface, it may seem disappointing that there is no automatic change management, but this allows the library to be efficient and small.

You may be wondering how to monitor for changes to the objects. The answer is in the generated code. In each of the generated data contract classes, there are partial methods that get called as the data in the class changes. (See go.microsoft.com/fwlink/?LinkId=122979 for more information on partial methods.) If these methods are never implemented, the classes do not incur any overhead from this change notification. You can use the partial method mechanism on any of the data contracts that support change to implement change notification. Simply call the DataService­Contract in partial methods; coupling the DataServiceContract class is not necessary.

Luckily, Silverlight already supports an interface for change notification (INotifyPropertyChange). Leverage this interface in your implementation for notifying anyone interested in a change to your data. For example, you could implement INotify­PropertyChange in your data contract classes (in our case, Product class) to define an event that can be fired as the object changes. Here's how it looks:

public partial class Product : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;
}

Implementing this allows you to raise an event as any property changes. You can determine when to raise this event by implementing the partial methods that the generated data contract classes call. For example, to notify subscribers when the ProductName property changes, just implement the OnProductNameChanged partial method and then fire the PropertyChanged event, passing in ProductName to identify the changed property to event subscribers. Here's the code:

partial void OnProductNameChanged()
{
  if (PropertyChanged != null)
  {
    PropertyChanged(this, new PropertyChangedEventArgs("ProductName"));
  }  
}

By implementing these partial methods on all writable properties, you have a simple way to monitor changes to your object. Then you can just register for the PropertyChanged event and notify the DataServiceContext object as the object changes:

// In the OnLoadComplete method
// Get the results and add them to the collection
List<Product> products = productQuery.EndExecute(result).ToList();

foreach (Product product in products)
{
  // Wireup Change Notification
  product.PropertyChanged += 
    new PropertyChangedEventHandler(product_PropertyChanged);
}

Finally, you can implement the product_PropertyChanged method to notify the DataServiceContext object:

void product_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
  Product product = (Product)sender;
  TheContext.UpdateObject(product);
}

Similarly, as objects are created or deleted, you need to notify the DataServiceContext, as you can see in Figure 8 .

Figure 8 Create and Delete Notifications

void addNewButton_Click(object sender, RoutedEventArgs e)
{
  Product theProduct = new Product();
  // ...
  TheContext.AddObject(theProduct);
}

void deleteButton_Click(object sender, RoutedEventArgs e)
{
  Product theProduct = (Product)theList.SelectItem;
  TheContext.DeleteObject(theProduct);
  theCollection.Remove(theProduct);
}

With all this code in place, you can change the objects in your Silverlight UI and let data binding and your change notification code make sure the DataServiceContext knows about any changes that will occur. But how do you execute actual updates to the service?

Updating through the Service

Now that your DataServiceContext object knows about data changes, you need a way to communicate them to the server. To support this, the DataServiceContext class has a BeginSaveChanges method, which follows the same asynchronous model as the queries described earlier in this article. The BeginSaveChanges method takes all the changes in the DataServiceContext and sends them to the server, as seen here:

TheContext.BeginSaveChanges(SaveChangesOptions.None, 
                            new AsyncCallback(OnSaveAllComplete), 
                            null);

When issuing the BeginSaveChanges, there is a flagged enumeration called SaveChangesOptions. This enumeration allows you to specify two options: whether to use batching and whether to continue even if some objects fail to save. In general, I would suggest always using batching. In fact, batching is required for some types of parent/child relationships to update correctly if the server has particular referential integrity constraints in place.

Once the save is complete, the callback will be executed. There are two mechanisms that propagate error information for you. First, if there is an exception thrown during the execution of the save, this exception will be thrown when you call "EndSaveChanges"­ in the callback. Because of this, you will want to use a try/catch block to catch catastrophic errors. In addition, the return type from EndSaveChanges is a DataServiceResponse object. The DataService­Response has a HasErrors property (see Figure 9 ), but it is unreliable in the Silverlight 2 Beta 2 version of the library.

Figure 9 BeginSaveChanges Callback

void OnSaveAllComplete(IAsyncResult result)
{
  bool succeeded = true;
  try
  {
    DataServiceResponse response = 
      (DataServiceResponse)TheContext.EndSaveChanges(result);

    foreach (OperationResponse opResponse in response)
    {
      if (opResponse.HasErrors)
      {
        succeeded = false;
      }
    }

  }
  catch (Exception ex)
  {
    succeeded = false;
  }

  // Alert the User
}

Instead of depending on that, you can iterate through the actual OperationResponse objects (the DataServiceResponse is a collection of OperationResponse objects) to see if there are errors on each of the responses returned from the server. In later versions, you should be able to rely on the HasErrors property of the DataServiceResponse class itself.

Debugging the Service

In debugging the service, there are three important tasks you will want to perform: viewing the state of data in the DataService­Context object, viewing the requests made by ADO.NET Data Services, and, lastly, catching server errors.

First, let's deal with the state of entities in the DataService­Context object. The DataServiceContext class exposes two collections that are useful: Entities and Links. These collections are read-only collections of the entities and links between entities that are being tracked by the DataServiceContext. When debugging, whether you are marking objects as changed or not, viewing these collections in the debugger is invaluable to determine whether or not your change-tracking code is working correctly.

Note that it is also important for you to view the actual server requests that are being made by your Silverlight 2 application. The best way to do that is with a network proxy of some sort. I use Fiddler2 ( fiddler2.com ) for this. If you're not familiar with Fiddler2, it's basically a tool for watching Web traffic so you can see what is actually happening on the wire.

For ADO.NET Data Services, you want to see the actual communication over the wire to see what was being sent to and from the Silverlight app. Please refer to my blog ( wildermuth.com/2008/06/07/Debugging_ADO_NET_Data_Services_with_Fiddler2 ) for more.

Lastly, in the latest .NET Framework 3.5 SP1, server-side errors are not being propagated to the client well. In fact, most of the errors on the server are being swallowed by the server. The best tactic for debugging server errors is to use the Exception option in the Debug menu (Debug->Exceptions…) and configure the debugger to stop on all .NET Exceptions. Once you select this option, you can see the exceptions thrown by the service (though you may need to "Continue" through other first-chance exceptions).

Where We Are

My goal here has been to show how ADO.NET Data Services is the gateway between Silverlight 2 and server-based models. You should now know how to use ADO.NET Data Services to read and write data from the server without resorting to hand-designed Web services. As you can see, the combination of Silverlight, ADO.NET Data Services, and LINQ lets you build powerful data-driven Web applications with all the advantages of Web 2.0 technologies. For more on these technologies, see the "Further Reading" sidebar.

Shawn Wildermuth is a Microsoft MVP (C#) and the founder of Wildermuth Consulting Services. He is the author of several books and numerous articles. In addition, Shawn currently runs the Silverlight Tour teaching Silverlight 2 around the country. He can be contacted at shawn@wildermuthconsulting.com .

Further Reading