Chapter 44

WCF Data Services

WHAT’S IN THIS CHAPTER?

  • Overview of WCF Data Services
  • WCF Data Services hosting with CLR objects
  • HTTP client access to WCF Data Services
  • URL queries to WCF Data Services
  • WCF Data Services with the ADO.NET Entity Framework
  • Using the WCF Data Services Client Library
  • Tracking, updates, and batching

WROX.COM CODE DOWNLOADS FOR THIS CHAPTER

The wrox.com code downloads for this chapter are found at http://www.wrox.com/remtitle.cgi?isbn=1118314425 on the Download Code tab. The code for this chapter is divided into the following major examples:

  • Data Services Samples
    • Data Services Host
    • Web Request Client
  • EDM Data Services Samples
    • Restaurant Data Services Web
    • Client App

OVERVIEW

WCF Data Services is based on WCF, which was covered in Chapter 43, “Windows Communication Foundation,” and it also makes a lot of use of the ADO.NET Entity Framework, that was covered in Chapter 33, “ADO.NET Entity Framework.” In that chapter, the ADO.NET Entity Framework was used to easily create an object model to map to the database structures. The Entity Framework does not provide a way to get the objects across different tiers; however, this is where WCF Data Services come into play. WCF Data Services offers a WCF service to easily access data provided by an Entity Data Model or by simple CLR objects implementing the IQueryable<T> interface.

The main namespace used with this chapter is System.Data.Services. A new technology that partly offers the same functionality as WCF Data Services but is not as mature as WCF Data Services is ASP.NET Web API. This technology is covered in Chapter 41, “ASP.NET MVC.”

The ADO.NET Entity Framework offers mapping between objects and relational databases and creates entity classes. The data context of the Entity Framework stays informed about changes to data so that it knows what should be updated, but the Entity Framework does not help when creating solutions over multiple tiers.

Using WCF Data Services, you can use the Entity Framework (or a simple CLR object model) on the server side and send HTTP queries from the client to the service to retrieve and update data. Figure 44-1 shows a typical scenario with a Windows client or a Web page using HTML and JavaScript to send an HTTP request to the server.

The returned information can be in AtomPub or JSON format. AtomPub is the Atom Publisher format based on XML. JSON (JavaScript Object Notation) is best accessed from JavaScript clients.

WCF Data Services makes use of WCF (Windows Communication Foundation) for the communication part and uses the WebHttpBinding.

With WCF Data Services you not only get features on the server and the capability to use HTTP Web requests with AtomPub or JSON, there is also a client-side part of WCF Data Services. For the client, there’s a data service context and the possibility to create queries that are transformed in the AtomPub or JSON format. Whereas the HTTP protocol is stateless, the data service context for the client is stateful. With this context, the client can keep track of what entities are changed, added, or removed, and send a request with all the change information to the service. In the following section, we’ll get into the details of WCF Data Services by first creating a simple service that is accessed from a client using HTTP Web requests.

CUSTOM HOSTING WITH CLR OBJECTS

The heart of WCF Data Services is the DataService<T> class, which enables implementation of a WCF service. DataService<T> implements the interface IRequestHandler, which is defined as follows. The attribute WebInvoke is specified to accept any URI parameters and any HTTP methods. With the parameter and return type, the method ProcessRequestForMessage is very flexible. It accepts any stream and returns a message. This is a requirement for the flexibility of data supported.

[ServiceContract]
public interface IRequestHandler
{
  [OperationContract]
  [WebInvoke(UriTemplate="*", Method="*")]
  Message ProcessRequestForMessage(Stream messageBody);
}

NOTE The WCF attributes ServiceContract, OperationContract, and WebInvoke are explained in Chapter 43, “Windows Communication Foundation.”

This section begins with a simple example by using a console application to host a service that offers a list of CLR objects. This service will then be used from a client application that directly sends HTTP requests to retrieve the data.

CLR Objects

The sample defines two entity classes: Category and Menu. These classes are simple data holders. The Menu class contains Name, Price, and a reference to the Category class. To make the different instances uniquely identifiable by Data Services, the attribute DataServiceKey must be added to reference the unique identifier. This attribute is defined in the namespace System.Data.Services.Common. Instead of defining a single property as the identity, it is also possible to assign a list of properties for unique identification. The Category class is defined in the code file DataServicesSamples/DataServicesHost/Category.cs:

  [DataServiceKey("Id")]
  public class Category
  {
    public int Id { get; set; }
    public string Name { get; set; }
        
    public Category() { }
    public Category(int id, string name)
    {
      this.Id = id;
      this.Name = name;
    }
  }

The Menu class is defined in the code file DataServicesSamples/DataServicesHost/Menu.cs:

  [DataServiceKey("Id")]
  public class Menu
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public Category Category { get; set; }
        
    public Menu() { }
    public Menu(int id, string name, decimal price, Category category)
    {
      this.Id = id;
      this.Name = name;
      this.Price = price;
      this.Category = category;
    }
  }

The class MenuCard manages the collections of Menu and Category items. It contains a list of Menu and Category items that can be iterated from the public Menus and Categories properties. This class implements a singleton pattern, so only one list of each exists (code file DataServicesSamples/DataServicesHost/MenuCard.cs):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Wrox.ProCSharp.DataServices
{
  public class MenuCard
  {
    private static object sync = new object();
    private static MenuCard menuCard;
    public static MenuCard Instance
    {
      get
      {
        lock (sync)
        {
          if (menuCard == null)
            menuCard = new MenuCard();
        }
        return menuCard;
      }
    }
        
    private readonly List<Category> categories;
    private readonly List<Menu> menus;
        
    private MenuCard()
    {
      categories = new List<Category>
      {
        new Category(1, "Main"),
        new Category(2, "Appetizer")
      };
        
      menus = new List<Menu>() 
      {
        new Menu(1, "Roasted Chicken", 22, categories[0]),
        new Menu(2, "Rack of Lamb", 32, categories[0]),
        new Menu(3, "Pork Tenderloin", 23, categories[0]),
        new Menu(4, "Fried Calamari", 9, categories[1])
      };
    }
        
    public IEnumerable<Menu> Menus
    {
      get
      {
        return menus;
      }
    }
        
    public IEnumerable<Category> Categories
    {
      get
      {
        return categories;
      }
    }
  }
}

Data Model

Now it gets really interesting with the class MenuCardDataModel. This class defines what entities are offered from the data service by specifying properties that return IQueryable<T>. IQueryable<T> is used by the DataService<T> class to pass expressions for querying in object lists (code file DataServicesSamples/DataServicesHost/MenuCardDataModel.cs):

  public class MenuCardDataModel
  {
    public IQueryable<Menu> Menus
    {
      get
      {
        return MenuCard.Instance.Menus.AsQueryable();
      }
    }
        
    public IQueryable<Category> Categories
    {
      get
      {
        return MenuCard.Instance.Categories.AsQueryable();
      }
    }
  }

Data Service

The implementation of the data service MenuDataService derives from the base class DataService<T>. The generic parameter of the DataService<T> class is the class MenuCardDataModel, with the Menus and Categories properties returning IQueryable<T>.

In the IntializeService method, you need to configure the entity and service operations access rules by using the DataServiceConfiguration class. You can pass * for the entity and operations access rules to allow access to every entity and operation. With the enumerations EntitySetRights and ServiceOperationsRights, you can specify whether read and/or write access should be enabled. The MaxProtocolVersion property of the DataServiceBehavior defines what version of the AtomPub protocol should be supported. Version 2 is supported since .NET 4.0 and includes support for some additional features, such as getting the number of items from a list (code file DataServicesSamples/DataServicesHost/MenuDataService.cs):

using System.Data.Services;
using System.Data.Services.Common;
using System.Linq;
using System.ServiceModel.Web;
        
namespace Wrox.ProCSharp.DataServices
{
  public class MenuDataService : DataService<MenuCardDataModel>
  {
    public static void InitializeService(DataServiceConfiguration config)
    {
      config.SetEntitySetAccessRule("Menus", EntitySetRights.AllRead);
      config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
        
      config.DataServiceBehavior.MaxProtocolVersion = 
          DataServiceProtocolVersion.V2;
    }
  }
}

Hosting the Service

Finally, you need a process to host the application. Later in this chapter, a web application is used for hosting. You can also host the service in any application type that’s supported by WCF; this can be a simple console application or a Windows Service. This sample uses a console application that you can easily change to any other hosting type.

In the Main method of the console application, a DataServiceHost is instantiated. DataServiceHost derives from the base class ServiceHost to offer WCF functionality. You can also use the DataServiceHostFactory to create the DataServiceHost. Calling the Open method, DataServiceHost instantiates an instance from the MenuDataService class for offering the service functionality. The address of the service is defined with http://localhost:9000/Samples (code file DataServicesSamples/DataServicesHost/Program.cs):

using System;
using System.Data.Services;
using System.ServiceModel;
        
namespace Wrox.ProCSharp.DataServices
{
  class Program
  {
    static void Main()
    {
      DataServiceHost host = null;
      try
      {
        host = new DataServiceHost(typeof(MenuDataService),
          new Uri[] { new Uri("http://localhost:9000/Samples ") });
        
        host.Open();
        
        Console.WriteLine("service running");
        Console.WriteLine("Press return to exit");
        Console.ReadLine();        
      }
      catch (CommunicationException ex)
      {
        Console.WriteLine(ex.Message);
      }
      finally
      {
        if (host.State == CommunicationState.Opened)
          host.Close();
      }
    }
  }
}

Now you can start the executable and request the service with the links http://localhost:9000/Samples/Menus and http://localhost:9000/Samples/Categories from within Internet Explorer. To use Internet Explorer to see the data returned from the service, you need to deselect the option “Turn on feed reading view.”


NOTE To start a listener without elevated administrator rights, you need to configure the ACL for the port and the user as follows:
netsh http add urlacl url=http://+:9000/ Samples user=username 
  listen=yes
Of course, changing these administrative settings requires elevated administrator rights.

Additional Service Operations

Instead of offering only the properties from the data model, you can add additional service operations to the data service. In the following sample code, you can see the method GetMenusByName, which gets a request parameter from the client and returns all menus starting with the requested string with an IQueryable<Menu> collection. Such operations need to be added to the service operation access rules in the InitalizeService method in case you do not offer all service operations using the * (code file DataServicesSamples/DataServicesHost/MenuDataService.cs):

  public class MenuDataService : DataService<MenuCardDataModel>
  {
    public static void InitializeService(DataServiceConfiguration config)
    {
      config.SetEntitySetAccessRule("Menus", EntitySetRights.AllRead);
      config.SetEntitySetAccessRule("Categories", EntitySetRights.AllRead);
      config.SetServiceOperationAccessRule("GetMenusByName",
          ServiceOperationRights.AllRead);
        
      config.DataServiceBehavior.MaxProtocolVersion = 
           DataServiceProtocolVersion.V2;
    }
        
    [WebGet(UriTemplate="GetMenusByName?name={name}",
            BodyStyle=WebMessageBodyStyle.Bare)]
    public IQueryable<Menu> GetMenusByName(string name)
    {
      return (from m in CurrentDataSource.Menus
              where m.Name.StartsWith(name)
              select m).AsQueryable();
    }
  }

HTTP CLIENT APPLICATION

The client application can be a simple application to just send HTTP requests to the service and receive AtomPub or JSON responses. The first client application example is a WPF application that makes use of the HttpClient class from the System.Net namespace.

Figure 44-2 shows the UI from the Visual Studio Designer, and Figure 44-3 provides details about the document outline. A TextBox control at the top of Figure 44-2 is used to enter the HTTP request. The TextBlock control receives the answer from the service. You can also see a CheckBox control, where the response can be requested in the JSON format. The Call Data Service button has the Click event associated with the OnRequest method.

The TextBox, TextBlock, and CheckBox XAML elements are bound to properties of the window, Result, UrlRequest, and JsonRequest. The window implements the interface INotifyPropertyChanged for data binding as shown in the following code snippet (code file DataServicesSample/WebRequestClient.xaml.cs):

   private string result;
    public string Result
    {
      get { return result; }
      set {
        SetProperty(ref result, value);
      }
    }
 
    private string urlRequest;
    public string UrlRequest
    {
      get
      {
        return urlRequest;
      }
      set
      {
        SetProperty(ref urlRequest, value);
      }
    }
 
    private bool? jsonRequest;
    public bool? JsonRequest
    {
      get
      {
        return jsonRequest;
      }
      set
      {
        SetProperty(ref jsonRequest, value);
      }
    }
 
    private void OnPropertyChanged(string propertyName)
    {
      var handler = PropertyChanged;
      if (handler != null)
      {
        handler(this, new PropertyChangedEventArgs(propertyName));
      }
    }
 
    private void SetProperty<T>(ref T field, T value, 
        [CallerMemberName] string propertyName = "")
    {
      if (!EqualityComparer<T>.Default.Equals(field, value))
      {
        field = value;
        OnPropertyChanged(propertyName);
      }
    }

In the implementation of the OnRequest handler, an HttpClient object is instantiated. HttpClient is new with .NET 4.5. The HTTP request that is sent is defined by the UrlRequest property. If the JSON check box is selected, along with the HTTP headers that are sent to the service, the Accept header is set to application/json. This way, the data service returns a JSON response instead of the default AtomPub format. The HTTP GET request to the server is sent with the asynchronous method GetAsync. When a response from the service is received, the Result property is set:

    private void OnRequest(object sender, RoutedEventArgs e)
    {
      Cursor oldCursor = this.Cursor;
      try
      {
        Result = string.Empty;
        this.Cursor = Cursors.Wait;
 
        using (var client = new HttpClient())
        {
          if (JsonRequest == true)
          {
            client.DefaultRequestHeaders.Accept.Add(
                MediaTypeWithQualityHeaderValue.Parse("application/json"));
          }
          using (HttpResponseMessage response =
              await client.GetAsync(UrlRequest))
          {
            Result = await response.Content.ReadAsStringAsync();
          }
        }
      }
      catch (InvalidOperationException ex)
      {
        Result = ex.Message;
      }
      catch (HttpRequestException ex)
      {
        Result = ex.Message;
      }
      catch (UriFormatException ex)
      {
        Result = ex.Message;
      }
      finally
      {
        this.Cursor = oldCursor;
      }
    }

Now you can use several HTTP requests to the service and see the data returned. The running application is shown in Figure 44-4.

Using the request http://localhost:9000/Samples/Menus(3) to get the menu with the unique identifier 3, the following AtomPub information is received:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<entry xml:base="http://localhost:9000/Samples/"
       xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
       xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
       xmlns="http://www.w3.org/2005/Atom">
  <id>http://localhost:9000/Samples/Menus(3)</id>
  <title type="text"></title>
  <updated>2012-02-03T15:49:49Z</updated>
  <author>
    <name />
  </author>
  <link rel="edit" title="Menu" href="Menus(3)" />
  <link rel=
    "http://schemas.microsoft.com/ado/2007/08/dataservices/related/Category"
    type="application/atom+xml;type=entry" title="Category"
    href="Menus(3)/Category" />
  <category term="Wrox.ProCSharp.DataServices.Menu"
    scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
  <content type="application/xml">
    <m:properties>
      <d:Id m:type="Edm.Int32">3</d:Id>
      <d:Name>Pork Tenderloin</d:Name>
      <d:Description m:null="true" />
      <d:Price m:type="Edm.Decimal">23</d:Price>
    </m:properties>
  </content>
</entry>

If you select the JSON format, a response with the same information but a JSON representation that can be easily read from JavaScript is returned:

{ "d" :
   { "__metadata":
      { "uri": "http://localhost:9000/Samples/Menus(3)",
        "type": "Wrox.ProCSharp.DataServices.Menu"
      },
     "Id": 3, "Name": "Pork Tenderloin", "Price": "23",
     "Category":
        { "__deferred":
           { "uri": "http://localhost:9000/Samples/Menus(3)/Category"
           }
        }
   }
}

In the next section, you’ll take a look at all the addressing options you have to build the query.

QUERIES WITH URLS

With the flexibility of the data services interface, you can request all objects from the service or get into specific objects and even values for specific properties.


NOTE For better readability, the following queries omit the address of the service, http://localhost:9000/Samples. This must be prefixed to all the queries.

You’ve already seen that you can get a list of all entities in an entity set. The query:

Menus

returns all menu entities, while:

Categories

returns all category entities. According to the AtomPub protocol, the returned root element is <feed> and contains <entry> elements for every element. This query does not work across references; for example, getting all menus with Menus doesn’t return the content of the category, only a reference to it. To get the category information within a menu, you can use the $expand query string:

Menus?$expand=Category

Passing the primary key value inside brackets returns just a single entity. In the next example, the menu with identifier 3 is accessed. This requires the definition of the DataServiceKey attribute used earlier:

Menus(3)

With a navigation property (using /), you can access a property of an entity:

Menus(3)/Price

The same syntax works for relations, accessing properties from related entities:

Menus(3)/Category/Name

To get just the value without the surrounding XML content of an entity, you can use the $value query function:

Menus(3)/Category/Name/$value

Getting back to complete lists, you can get the number of entities in a list with $count (note that $count is only available with V2 of the AtomPub protocol):

Menus/$count

Getting just the first entities of a list is done with the query string option $top:

Menus?$top=2

You can skip a number of entities with $skip (both $skip and $top can be combined for implementing paging functionality):

Menus?$skip=2

Filtering entities can be performed with the $filter query string option and by using the logical operators eq (equal), ne (not equal), gt (greater than), and ge (greater than or equal to):

Menus?$filter=Category/Name eq 'Appetizer'

You can sort the result with the $orderby query string option:

Menus?$filter=Category/Name eq 'Appetizer'&$orderby=Price desc

To only get a projection, a subset of the available properties, $select can be used to specify the properties that should be accessed:

Menus?$select=Name, Price

USING WCF DATA SERVICES WITH THE ADO.NET ENTITY FRAMEWORK

Now that you’ve learned the basic concept of Data Services, passing AtomPub or JSON data across simple HTTP requests, let’s get into a more complex example, using the ADO.NET Entity Framework for the data model, a web application for hosting, and clients performing LINQ queries across a network that makes use of classes from the System.Data.Services.Client namespace.

ASP.NET Hosting and EDM

First you have to create a new project. This time a Web Application project named RestaurantDataServiceWeb is used to host the service. The new data model is created with the help of the ADO.NET Entity Data Model (EDM) template and uses the tables Menus and Categories from a Restaurant database, as shown in Figure 44-5, to create the entity classes Menu and Category.


NOTE Chapter 33, “ADO.NET Entity Framework,” explains how to create and use ADO.NET Entity Data Models.

Now, use the Data Service template and create RestaurantDataService.svc. The .svc file contains the ASP.NET ServiceHost directive and uses the DataServiceHostFactory to instantiate the data service on request (markup file EDMDataServicesSamples/RestaurantDataServiceWeb/RestaurantDataService.svc):

<%@ ServiceHost Language="C#"  
    Factory="System.Data.Services.DataServiceHostFactory,
        System.Data.Services, Version=4.0.0.0, Culture=neutral,
        PublicKeyToken=b77a5c561934e089"
    Service="Wrox.ProCSharp.DataServices.RestaurantDataService" %>

With the code-behind, you need to change the template parameter of the DataService<T> class to reference the previously created entity data service context class, and change the entity set access rule and service operations access rule to allow access (code file EDMDataServicesSamples/RestaurantDataServiceWeb/RestaurantDataService.svc.cs):

using System.Data.Services;
using System.Data.Services.Common;
        
namespace Wrox.ProCSharp.DataServices
{
  public class RestaurantDataService : DataService<RestaurantEntities>
  {
    // This method is called only once to initialize service-wide policies.
    public static void InitializeService(DataServiceConfiguration config)
    {
      config.SetEntitySetAccessRule("Menus", EntitySetRights.All);
      config.SetEntitySetAccessRule("Categories", EntitySetRights.All);
      config.DataServiceBehavior.MaxProtocolVersion = 
          DataServiceProtocolVersion.V2;
    }
  }
}

Now, you can use a web browser to invoke queries as before to this data service; for example, you can use http://localhost:13617/RestaurantDataService.svc/Menus to receive the AtomPub of all menus from the database. Next, you create a client application that makes use of the client part of Data Services.


NOTE With a large database you shouldn’t return all the items with a query. Of course, the client can restrict the query to request a specified limited number of items. However, can you trust the client? With configuration options you can restrict limits on the server. For example, by setting config.MaxResultsPerCollection you can restrict the number of items that are returned from a collection to a specified maximum. You can also configure the maximum batch count, the maximum number of objects on an insert, and the maximum depth of objects in the tree. Alternatively, to allow any query, you can define service operations as shown in the section “Additional Service Operations.”

Using the WCF Data Service Client Library

Earlier in this chapter, you read how a .NET client application can be created that simply sends HTTP requests by using the HttpClient class. The client part of Data Services, with the namespace System.Data.Services.Client, offers functionality for the client to build HTTP requests. The two most important classes with this namespace are DataServiceContext and DataServiceQuery<TElement>. DataServiceContext represents the state that is managed on the client. This state keeps track of objects that are loaded from the server as well as all changes made on the client. DataServiceQuery<TElement> represents an HTTP query to a data service.

To call the data service, create a WPF application. Figure 44-6 shows the design view of the WPF application. The top row contains a ComboBox control, which is used to display all categories. The second row contains a StackPanel with four Button controls. The third row contains a DataGrid control to display the menus, and the fourth row contains a TextBlock element to display some status information.

To create a client proxy and entity classes to be used on the client, you need metadata information. The Data Service offers metadata by using the $metadata query string:

http://localhost:13617/RestaurantDataService.svc/$metadata.

With this information a service reference can be added to the client application project to create a proxy class and entity classes. With the Restaurant data service, the class RestaurantEntities that derives from the base class DataServiceContext is created, which can be used as a proxy. Entity classes Menu and Category to keep the data are created as well. The entity classes implement the interface INotifyPropertyChanged, which makes it easy to be kept informed about changes from the UI.

Data Service Context

Now you can use the data service context RestaurantEntities to send a query to the data service. A variable of the service context is defined in the code-behind of the WPF window, in the class MainWindow. To avoid conflict with the Menu class that’s generated for the data service and the Menu control from WPF, a namespace alias is defined with the alias name R, for Restaurant, to reference the generated classes from the service reference (code file EDMDataServicesSamples/ClientApp/MainWindow.xaml.cs):

using System;
using System.Collections.Generic;
using System.Data.Services.Client;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using R = Wrox.ProCSharp.DataServices.RestaurantService;
        
namespace Wrox.ProCSharp.DataServices
{
  public partial class MainWindow : Window
  {
    private R.RestaurantEntities data;
    private DataServiceCollection<R.Menu> trackedMenus;

The instance of the RestaurantEntities is created in the constructor of the MainWindow class. The constructor of the data service context requires a link to the service root. This is defined within the application configuration file and accessed from the strongly typed settings:

    public MainWindow()
    {
      var serviceRoot = new Uri(Properties.Settings.Default.RestaurantServiceURL);
      data = new R.RestaurantEntities(serviceRoot);
      data.SendingRequest += data_SendingRequest;
        
      InitializeComponent();
      this.DataContext = this;
    }

The content of the application configuration file used to reference the data service is shown here (config file EDMDataServicesSamples/ClientApp/app.config):

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <sectionGroup name="applicationSettings"
        type="System.Configuration.ApplicationSettingsGroup, System, 
            Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
      <section name="Wrox.ProCSharp.DataServices.Properties.Settings"
          type="System.Configuration.ClientSettingsSection, System, 
              Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
              requirePermission="false" />
    </sectionGroup>
  </configSections>
  <applicationSettings>
    <Wrox.ProCSharp.DataServices.Properties.Settings>
      <setting name="RestaurantServiceURL" serializeAs="String">
        <value>http://localhost:13617/RestaurantDataService.svc</value>
      </setting>
    </Wrox.ProCSharp.DataServices.Properties.Settings>
  </applicationSettings>
</configuration>

With the SendingRequest event, the data service context invokes the handler every time a request is sent to the service. The method data_sendingRequest is associated with the SendingRequest event and receives information about the request in the SendingRequestEventArgs argument. With SendingRequestEventArgs, you can access request and header information. The request method and URI retrieved from the Method and RequestUri properties is written to the textStatus control in the UI (code file EDMDataServicesSamples/ClientApp/MainWindow.xaml.cs):

      void data_SendingRequest(object sender, SendingRequestEventArgs e)
      {
        var sb = new StringBuilder();
        sb.AppendFormat("Method: {0}
", e.Request.Method);
        sb.AppendFormat("Uri: {0}
", e.Request.RequestUri.ToString());
        this.textStatus.Text = sb.ToString();
      }

The data service context RestaurantEntities enables you to retrieve entities from the service; tracks the entities that have been retrieved; enables you to add, delete, and change entities inside the data context; and keeps the state of the changes for sending update requests.

LINQ Query

The data service context implements a LINQ provider to convert LINQ requests to HTTP requests. The Categories property of the MainWindow class defines a LINQ query by using the data context to return all categories:

    public IEnumerable<R.Category> Categories
    {
      get
      {
        return from c in data.Categories
               orderby c.Name
               select c;
      }
    }

The ComboBox in the XAML code defines a binding to this property to display all categories (XAML file EDMDataServicesSamples/ClientApp/MainWindow.xaml):

    <ComboBox x:Name="comboCategories" Grid.Row="0"
              ItemsSource="{Binding Path=Categories}" SelectedIndex="0"
              SelectionChanged="OnCategorySelection">
      <ComboBox.ItemTemplate>
        <DataTemplate>
          <TextBlock Text="{Binding Path=Name}" />
        </DataTemplate>
      </ComboBox.ItemTemplate>
    </ComboBox>

With the help of the SendingRequest event, the LINQ query to access all categories is converted to an HTTP GET request with the following URI:

Method: GET
Uri: http://localhost:13617/RestaurantDataService.svc/Categories()?$orderby=Name

The Categories property of the RestaurantEntities class returns a DataServiceQuery<Category>. DataServiceQuery<T> implements IQueryable, and thus the compiler creates expression trees from the LINQ query that are analyzed and converted to an HTTP GET request.


NOTE LINQ queries are covered in Chapter 11, “Language Integrated Query.”

You can also create a LINQ query to get only the menus from the category Soups:

            var q = from m in data.Menus
                    where m.Category.Name == "Soups"
                    orderby m.Name
                    select m;

This translates the following URI:

filter=Category/Name eq 'Soups'&$orderby=Name.

The query can be expanded by using Data Services query options. For example, to include the relationship and get all categories with the selected menus, add AddQueryOption with $expand and the parameter value Category:

            var q = from m in data.Menus.AddQueryOption("$expand", "Category")
                    where m.Category.Name == "Soups"
                    orderby m.Name
                    select m;

This modifies the query as follows:

Menus()?$filter=Category/Name eq 'Soups'&$orderby=Name&$expand=Category

The DataServiceQuery<T> class also offers the Expand method for this particular query option:

            var q = from m in data.Menus.Expand("Category")
                    where m.Category.Name == "Soups"
                    orderby m.Name
                    select m;

Observable Collections

To keep the user interface informed about collection changes, Data Services contains the collection class DataServiceCollection<T>. This collection class is based on ObservableCollection<T>, which implements the interface INotifyCollectionChanged. WPF controls register with the event of this interface to keep informed about changes in the collection so that the UI can be updated immediately.

The Menus property creates a new DataServiceCollection<T> if one was not already created, and fills the collection with menus from a selected category by using a LINQ query. The retrieved entities are associated with the data service context data as the context is passed with the constructor of the DataServiceCollection<T> (code file EDMDataServicesSamples/ClientApp/MainWindow.xaml.cs):

    public IEnumerable<R.Menu> Menus
    {
      get
      {
        if (trackedMenus == null)
        {
          trackedMenus = new DataServiceCollection<R.Menu>(data);
 
          trackedMenus.Load(
              from m in data.Menus
              where m.CategoryId == 
                  (comboCategories.SelectedItem as R.Category).Id
                  && m.Active
              select m);
        }
        return trackedMenus;
      }
    }

The DataGrid from the XAML code maps to the Menus property with the Binding markup extension (XAML file EDMDataServicesSamples/ClientApp/MainWindow.xaml):

    <DataGrid Grid.Row="2" ItemsSource="{Binding Path=Menus}" 
              AutoGenerateColumns="False">
      <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding Path=Name}" />
        <DataGridTextColumn Binding="{Binding Path=Description}" />
        <DataGridTextColumn Binding="{Binding Path=Price}" />
        <DataGridTextColumn Binding="{Binding Path=CategoryId}" />
      </DataGrid.Columns>
    </DataGrid>

Using the SelectionChanged event from the ComboBox to select a new category, the menus are retrieved again with the newly selected category in the handler method OnCategorySelection (code file EDMDataServicesSamples/ClientApp/MainWindow.xaml.cs):

    private void OnCategorySelection(object sender, SelectionChangedEventArgs e)
    {        
      var selectedCategory = comboCategories.SelectedItem as R.Category;
      if (selectedCategory != null && trackedMenus != null)
      {
        trackedMenus.Clear();
        trackedMenus.Load(from m in data.Menus
                          where m.CategoryId == selectedCategory.Id
                          select m);
      }
    }

NOTE For more information about observable collections and the class ObservableCollection<T>, please consult Chapter 10, “Collections.”

Object Tracking

The data service context keeps track of all the objects that have been retrieved. You can get information about the objects by iterating through the return of the Entities property. Entities returns a read-only collection of EntityDescriptor objects. The EntityDescriptor contains the entity itself, which can be accessed via the Entity property, and state information. The state of type EntityStates is an enumeration with the possible values Added, Deleted, Detached, Modified, or Unchanged. This information is used to keep track of changes and send a change request to the service.

To get information about the current entities associated with the data context, the handler method OnShowEntities is associated with the Click event of the Show Entities button. In the following code, the State, Identity, and Entity properties are used to write status information to the UI:

    private void OnShowEntities(object sender, RoutedEventArgs e)
    {
      var sb = new StringBuilder();
      foreach (var entity in data.Entities)
      {
        sb.AppendFormat("state = {0}, Uri = {1}, Element = {2}
",
                        entity.State, entity.Identity, entity.Entity);
      }
      this.textStatus.Text = sb.ToString();
    }

Figure 44-7 shows the running application with information about the objects tracked.

Adding, Modifying, and Deleting Entities

To add new entities to the data service context and thus send the new objects later to the data service, you can add entities to the data service context with the AddObject method, or strongly typed variants such as AddToMenus and AddToCategories. You just have to fill the mandatory properties; otherwise, saving the state will not succeed.

Adding new objects sets the state to Added. The DeleteObject method of the data service context sets the state of an object to Deleted. If properties of an object are modified, the state changes from Unchanged to Modified.

Now you can invoke the method SaveChanges(), which sends HTTP MERGE requests to update entities, HTTP DELETE requests to delete entities, and HTTP POST requests to add new entities. The following code snippet invokes the asynchronous version of SaveChanges, BeginSaveChanges, and EndSaveChanges. This client proxy implements the async pattern that is converted to the task-based async pattern with the help of the method FromAsync:

    private async void OnSave(object sender, RoutedEventArgs e)
    {
      try
      {
        DataServiceResponse response = await Task<DataServiceResponse>.
          Factory.FromAsync(data.BeginSaveChanges, data.EndSaveChanges); 
      }
      catch (DataServiceRequestException ex)
      {
        textStatus.Text = ex.ToString();
      }
    }

Operation Batching

Instead of sending DELETE and MODIFY requests for every entity in a collection, you can batch multiple change requests to a single network request. By default, every change is sent by using a single request when the BeginSaveChanges method is applied. Adding the parameter SaveChangesOptions.Batch to the BeginSaveChanges method combines all the change requests to a single network call with the $batch query option:

    private async void OnSave(object sender, RoutedEventArgs e)
    {
      try
      {
        DataServiceResponse response = await Task<DataServiceResponse>.Factory.
          FromAsync<SaveChangesOptions>(data.BeginSaveChanges, 
          data.EndSaveChanges, SaveChangesOptions.Batch, null);
      }
      catch (DataServiceRequestException ex)
      {
        textStatus.Text = ex.ToString();
      }
    }

As the transferred data demonstrates, multiple HTTP headers are combined inside a single HTTP POST request, and split again on the server side. With the following HTTP POST request, you can see that the DELETE and MERGE requests are combined. The DELETE request deletes the menu with id 4; the MERGE request contains AtomPub information to update the menu with id 2:

POST /RestaurantDataService.svc/$batch HTTP/1.1
User-Agent: Microsoft WCF Data Services
DataServiceVersion: 1.0;NetFx
MaxDataServiceVersion: 2.0;NetFx
Accept: application/atom+xml,application/xml
Accept-Charset: UTF-8
Content-Type: multipart/mixed; boundary=batch_24448a55-e96f-4e88-853b-cdb5c1ddc8bd
Host: 127.0.0.1.:13617
Content-Length: 1742
Expect: 100-continue
        
--batch_24448a55-e96f-4e88-853b-cdb5c1ddc8bd
Content-Type: multipart/mixed;
boundary=changeset_8bc1382a-aceb-400b-9d19-dc2eec0e33b7
        
--changeset_8bc1382a-aceb-400b-9d19-dc2eec0e33b7
Content-Type: application/http
Content-Transfer-Encoding: binary
        
DELETE http://127.0.0.1.:13617/RestaurantDataService.svc/Menus(4) HTTP/1.1
Host: 127.0.0.1.:13617
Content-ID: 18
        
--changeset_8bc1382a-aceb-400b-9d19-dc2eec0e33b7
Content-Type: application/http
Content-Transfer-Encoding: binary
        
MERGE http://127.0.0.1.:13617/RestaurantDataService.svc/Menus(2) HTTP/1.1
Host: 127.0.0.1.:13617
Content-ID: 19
Content-Type: application/atom+xml;type=entry
Content-Length: 965
        
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<entry xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
xmlsn:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
xmlsn="http://www.w3.org/2005/Atom">
  <category scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme"
            term="RestaurantModel.Menu" />
  <title />
  <author>
    <name />
  </author>
  <updated>2009-08-01T19:17:43.4741882Z</updated>
  <id>http://127.0.0.1.:13617/RestaurantDataService.svc/Menus(2)</id>
  <content type="application/xml">
    <m:properties>
      <d:Active m:type="Edm.Boolean">true</d:Active>
      <d:Description>Lean and tender 8 oz. sirloin seasoned perfectly
                     with our own special seasonings and topped with seasoned
                     butter.
      </d:Description>
      <d:Id m:type="Edm.Int32">2</d:Id>
      <d:Name>Sirloin Steak</d:Name>
      <d:Price m:type="Edm.Decimal">44.0</d:Price>
    </m:properties>
  </content>
</entry>
--changeset_8bc1382a-aceb-400b-9d19-dc2eec0e33b7--
--batch_24448a55-e96f-4e88-853b-cdb5c1ddc8bd--

SUMMARY

This chapter described the features of WCF Data Services, which brings the data model from the ADO.NET Entity Framework across multiple tiers. The technology that it is based on is WCF, by using connectionless, stateless communication sending AtomPub or JSON queries.

You’ve not only seen the server-side part of this technology but also the client-side part, where change information is tracked inside a data service context. The client-side part of WCF Data Services implements a LINQ provider, so you can create simple LINQ requests that are converted to HTTP GET/POST/PUT/DELETE requests.

The next chapter gives you information about Windows Workflow Foundation (WF), which allows graphically assigning different activities to form a workflow. Such workflows can describe a WCF service, and thus not only be used to host a workflow but also a WCF service.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset