Share via


span.sup { vertical-align:text-top; }

Data 2.0

Expose And Consume Data in A Web Services World

Elisa Flasko and Mike Flasko

Code download available at:DataServices2008_08.exe(225 KB)

This article is based on a prerelease version of ASP.NET 3.5 and the Microsoft AJAX Library. All information herein is subject to change.

This article discusses:

  • The meaning of data services
  • Exposing and consuming data
  • Describing data with the Entity Data Model
  • Data security
This article uses the following technologies:
ADO.NET Data Services, LINQ, Entity Data Model

Contents

Describing Data with the Entity Data Model
Relational Data
What about Non-Relational Data?
Uniform URI Format
Data Service Security
Client Side: Accessing the Service
.NET Client Library
Silverlight Client Library
AJAX Client Library
Querying a Data Service
Command Mapping
Batching
Learning More

Remember back to the last Rich Internet Application (RIA) you built. How did you get your data? How did you separate that data from presentation and UI information sent to the browser? What if there was a simpler way to do this?

Separation of presentation and data is not a new idea, but with the growing popularity of RIA technologies such as AJAX and Silver­light™, it has become much more prevalent. These technologies are built on the idea of separating presentation and data to enable a more interactive and responsive application.

For example, Silverlight-based RIA applications pre-compile code that drives presentation and is deployed to the client via the Web server. Then, after reaching the Web browser, the code calls back to a Web server to retrieve the data to display in the user interface. Technologies such as these typically eliminate the option of a server-side rendering process that would mix data and presentation code.

In addition to separating presentation and data in order to drive richer, more interactive Web experiences, there is a trend on the Web to expose and consume standalone data, independent of any user interface. The proliferation of data-driven applications such as "mashups" indicates that the broad availability of interesting, readily consumable data is enabling new application scenarios.

Based on observation of these trends, the ADO.NET Data Services Framework began as a way to help developers looking to expose and consume data via services from their RIA applications. When exploring the space, two key ideas surfaced: building general-purpose client libraries and tools with the current approaches to data-centric services is a difficult concept in itself, and creating and maintaining those services requires a significant developer investment. In this article, we'll focus on what data services are and discuss a few of the key features. If you would like more insight into some of the ideas behind data services, see the post titled "Why ADO.NET Data Services?" on the team blog (go.microsoft.com/fwlink/?LinkId=120530).

In general, the goal of the ADO.NET Data Services Framework is to create a simple framework based on Representational State Transfer (REST) for exposing and consuming data-centric services. Such services expose data using a uniform interface to be consumed by Web clients across a corporate intranet or the Internet. The framework consists of a server library to expose data securely as a service and a set of client libraries built for a range of Microsoft applications and technologies (the Microsoft® .NET Framework, Silverlight, and so on) to consume services. Figure 1 illustrates the architecture.

fig01.gif

Figure 1 The ADO.NET Data Services Framework Architecture

Describing Data with the Entity Data Model

Each ADO.NET Data Service is described in Entity Data Model (EDM) terms using the Conceptual Schema Definition Language (CSDL). The EDM uses two primary concepts, entities and associations (or relationships). Entities are instances of Entity Types (such as Customer or Employee) which are structured records with a key. An entity key is formed from a subset of properties of the Entity Type. The key (Customer­Id and OrderId, respectively) uniquely identifies and allows updates to entity instances and also allows them to participate in relationships. Entities are grouped by EntitySets (Customers is a set of Customer instances). Associations form links between two or more Entity Types (such as Employee WorksFor Department).

Why was the EDM chosen as the data description language for ADO.NET Data Services? As it turns out, the EDM maps very well to the primary Web concepts of resources and links, making it an ideal candidate (entities for resources and associations for links).

The EDM was developed with the goal of becoming the core data model across a suite of developer and server technologies. With only one data model to maintain across numerous applications, application maintenance would be simplified. The EDM could be used to define a model not only for custom applications built on ADO.NET Data Services, but also as the input to reporting and visualization applications, intranet portal applications or workflow applications. ADO.NET Data Services is the second Microsoft technology to build on the EDM concept (the first, of course, was the ADO.NET Entity Framework). For further information on the EDM and the ADO.NET Entity Framework, see msdn.microsoft.com/data and "ADO.NET: Achieve Flexible Data Modeling with the Entity Framework" in the July 2008 issue of MSDN® Magazine at msdn.microsoft.com/magazine/700331.

Relational Data

By default, ADO.NET Data Services works closely with the ADO.NET Entity Framework to simplify the process of connecting to and exposing a data model over data stored in SQL Server® or other third-party databases (Oracle, DB2, MySQL, to name merely a few).

The Entity Framework raises the level of abstraction at which developers work with data, meaning that rather than coding against rows and columns, a higher-level conceptual model is defined (as an Entity Data Model) over the relational data and the application is then programmed in terms of this model. The application need only understand data in the shapes that make sense for the application and those shapes are expressed using a rich vocabulary that covers concepts such as inheritance, complex types, and explicit relationships.

In general, ADO.NET Data Services works by converting a request to perform an operation over a resource (such as an HTTP request operation over a URI) to the equivalent operation on the data model that the resource represents. In this case, since the data model is backed by a relational database, URIs are converted to Entity Framework Object Services method calls.

An existing data model, created using the Entity Framework, can be exposed with only a few steps in a Visual Studio® 2008 SP1 ASP.NET Web Application project. (Other project types and hosting mechanisms are also supported, such as the Windows® Communication Foundation, or WCF, WebServiceHost.) An ADO.NET Entity Data Model is first created or imported into an ASP.NET Web Application. With the model available, we can then add a new ADO.NET Data Service. The Add New Item wizard will generate a basic service and make it visible in the Solution Explorer. We can then hook up the service to the model by changing the class definition, as shown here:

using NorthwindModel;

public class NorthwindService : DataService <NorthwindEntities>
{
     public static void InitializeService(IDataServiceConfiguration config)
     {

     }

}

Additionally, since the service is initially locked down by default, we must do some work to set service-wide security policy to enable the service to be used. The following code sets security at the EntitySet level when the service is initialized:

public static void InitializeService(IDataServiceConfiguration config)
{
    config.SetEntitySetAccessRule("*", EntitySetRights.All);
}

As you can see, this is a very simple example of setting security for an ADO.NET Data Service. Other security methods and considerations will be discussed later.

At this point we have a working ADO.NET Data Service that we can build and run.

A simple way to test your service without a client app is to just point a Web browser at the entry point of the service, for example <host>/<vdir>/service.svc. The entry point of the service will return the response as XML containing the list of EntitySets exposed by the data service. To view Atom (the default format returned by an ADO.NET Data Service) in Internet Explorer®, you must first ensure that the Feed Reading View in Internet Explorer is turned off. This can be done from the Content tab of Tools | Internet Options.

What about Non-Relational Data?

For all other data sources or to use additional database access technologies (such as LINQ to SQL), a mechanism is provided that enables any data source to be exposed as an ADO.NET Data Service using a plug-in model.

In this case, URIs are converted into LINQ queries. Data Services enables this approach by mapping objects that implement the IQueryable interface to EntitySets. This means that ADO.NET Data Services is able to expose any data source that has an IQueryable provider written for it. In order to enable this, ADO.NET Data Services defines a mapping between CLR objects and artifacts of the EDM-based data model.

Shortly, we will walk through the creation of an ADO.NET Data Service based on a simple CLR object graph. The steps we'll illustrate produce a data service created over an in-memory collection of objects. In a typical production deployment, however, the IQuery­able properties shown representing EntitySets would not expose in-memory data, but rather would translate IQueryable expression trees to data source-specific queries. For example, LINQ to Entities translates IQueryable expression trees into SQL statements.

When creating a data service against a non-relational store, the data service is again created within an ASP.NET Web Application project. Note that this time, however, we skip the step used to create the ADO.NET Entity Data Model and begin by adding a new ADO.NET Data Service. The Add New Item wizard will generate a basic service, visible in the Solution Explorer. To continue with this simple example, we must now create the in-memory data that we plan to expose via our service. We do this by creating three classes, two of which are User and Contact, where each User will have a set of Contacts. The third class is a MyDataService class that exposes two public properties (Contacts and Users) as IQueryable<Contact> and IQueryable<User>, as seen in Figure 2.

Figure 2 Creating In-Memory IQueryable Data Source

public class User
{
     public int ID { get; set; }
     public string Name { get; set; }
     public IList<Contact> Contacts { get; set; }
}

public class Contact
{
     public int ID { get; set; }
     public string Name { get; set; }
     public string Email { get; set; }
}

public class MyDataService
{
     static User[] _users;
     static Contact[] _contacts;

     static MyDataService()
     {
        _users = new User[]{ new User{ID=1, Name="Mike"},
                 new User{ID=2, Name="Elisa"} };

         _contacts = new Contact[]{ new Contact{ID=1, Name="Joe", 
                              Email="Joe@contoso.com"},
                  new Contact{ID=2, Name="Bob", Email="Bob@contoso.com"},
                  new Contact{ID=3, Name="Sam", Email="Sam@contoso.com"},
                  new Contact{ID=4, Name="Carl", Email="Carl@contoso.com"}, 
                  new Contact{ID=5, Name="Abby", Email="Abby@contoso.com"},
                  new Contact{ID=6, Name="Annie", Email="Annie@contoso.com"},
                  };

         _users[0].Contacts = new List<Contact>();
         _users[0].Contacts.Add(_contacts[0]);
         _users[0].Contacts.Add(_contacts[1]);
         _users[0].Contacts.Add(_contacts[4]);

         _users[1].Contacts = new List<Contact>();
         _users[1].Contacts.Add(_contacts[2]);
         _users[1].Contacts.Add(_contacts[3]);
         _users[1].Contacts.Add(_contacts[5]);
    }

    public IQueryable<User> Users
    {
         get { return _users.AsQueryable<User>();}
    }

    public IQueryable<Contact> Contacts
    {
         get { return _contacts.AsQueryable<Contact>(); }
    }
}

Now that we have determined a data source, we return to familiar territory, changing the class definition to point to our data source, as seen here:

public class contacts : 
  DataService<ADONETDataServiceNonRelSample.MyDataService>
{
    public static void InitializeService(IDataServiceConfiguration config)
    {
          config.SetEntitySetAccessRule("*", EntitySetRights.All);
    }
}

Again, we must do some initial work to set security policy, since the initial service is locked down by default. The code shows the same security policy set at the EntitySet level, as we saw earlier. At this point, we again have a working ADO.NET Data Service that we can build and run. Testing the service can be accomplished by simply using Internet Explorer to browse to the service endpoint.

Uniform URI Format

The uniform Uniform Resource Identifier (URI) is one of the primary concepts that make up ADO.NET Data Services, which defines a relatively simple, yet expressive, URI format allowing applications to query for sets of entities or single entities, as well as traverse the relationships that exist between those entities. The structure of this URI enables agents and controls to understand easily how to navigate the data presented by a service.

When we began testing our initial services, we saw the basic URI format pointing to the data service itself. Building on this basic URI, we are able to query the service using the following format, like so:

https://<host>/<vdir>/<service.svc>/<EntitySet>[(<Key>)
[/<NavigationProperty>[(<Key>)/...]]] 

For example, continuing with our earlier Northwind example, if we add /Customers to the end of the data service URI, <host>/<vdir>/NorthwindService.svc/Customers, we return all of the customers in the Customers Entity Set, in this case all of the Customers in the Northwind database. If, on the other hand, we want to return a single customer entity from the Northwind service, we could use the entity's key value, since Customer is a single-key entity. By requesting the URI <host>/<vdir>/NorthwindService.svc/­Customers('ALFKI'), we return the single Customer with the key "ALFKI." The navigation property is appended to a single entity query URI in a similar manner. By appending /Customers('ALFKI')/Orders, we navigate the relationship in our model between Customers and Orders, returning all of the Orders associated with the Customer identified by the key "ALFKI."

The URI format described above allows for basic query and traversal of the entities in our store, but does not account for the need to further control the output of queries or place constraints on them. To accomplish those goals, we'll use a set of optional query string parameters supported by ADO.NET Data Services that include $filter, $expand, $orderby, $skip and $top. Query string parameters are appended to the URI following the ? character.

For example, to query all customers in London, we would append the service URI with /Customers?$filter=City eq 'London'. To query a specific customer (with key 'ALFKI') and retrieve the related sales orders, we would append the service URI with /Customers('ALFKI')?$expand=Orders. To sort results by City in ascending (asc) order, we would append the service URI with /Customers?$orderby=City asc (or desc for descending order).

For a complete list of the URI formats supported by ADO.NET Data Services, check out the Data Services documentation, accessible from go.microsoft.com/fwlink/?LinkId=120539.

Data Service Security

Security is always the first concern when discussing a Web-based technology, especially one that provides access to and manipulation of critical data. Therefore, security has been a primary consideration throughout the development cycle of data services.

To provide authentication, ADO.NET Data Services leverages much of the existing ASP.NET and WCF authentication infrastructure to allow for an integrated authentication experience across applications and through the service. If you have an ASP.NET site that uses authentication (either one of the built-in authentication providers or a custom provider that sets the HTTP context principal appropriately), ADO.NET Data Services can leverage your chosen mechanism to establish the current identity (principal) for any given request. Similarly, if your data service is hosted outside ASP.NET (WCF or via a custom host) the host may also choose to use any authentication mechanism so long as it provides APIs for a data service author to access the principal of the request.

By default, any new ADO.NET Data Service is fully locked down, with all entities, Service Operations, and metadata being inaccessible with no read or write access. That said, there are a number of mechanisms for controlling authorization policy and performing per-request validations against your data service.

One of the first steps that a data service developer will take is to open up access to resources exposed by the data service. In our examples, we have shown a simple case of setting read/write access policy for the entire service. This is done for each service using the InitializeService method. The policies set were EntitySet policies allowing or restricting access to the Entity Sets in the model and setting which Entity Sets and related associations are available from the metadata endpoint of the service. Although both policies shown in those examples opened access to the entire model for reading and writing, we could have been much more specific, setting different policy for different EntitySets and including composite policies when necessary, as shown in Figure 3.

Figure 3 Setting Service-Wide Access Policy

public class MyService : DataService<NorthwindEntities>
{
   public static void InitializeService(
      IDataServiceConfiguration config)
   {
       // '/Customers' entities are enabled for all read and write 
       // operations
       config.SetEntitySetAccessRule("Customers", 
          EntitySetRights.All);

       //  URI '/Orders' is disabled, but '/Orders(1)' is enabled for read 
       //  only 
       config.SetEntitySetAccessRule("Orders",
           EntitySetRights.ReadSingle);

       //  Can insert and update, but not delete Products
       config.SetEntitySetAccessRule("Products",
           EntitySetRights.WriteInsert |
           EntitySetRights.WriteUpdate);
   }
}

In many scenarios, data services will also be required to run validation logic when entities enter the data service (for inserts, updates, or deletes) and restrict access to entities on a per-request basis. For these scenarios, you can make use of interceptors, enabling a data service developer to plug in custom validation or access policy logic into the request/response processing pipeline of a data service. For example, if we want to enable customers to retrieve only their orders and not orders placed by other customers, we would need to implement a query interceptor, as seen in Figure 4. The query interceptor returns a predicate to be appended to the query that is then pushed to the data store, eliminating the need for additional trips to the data store to retrieve access control information.

Figure 4 Query Interceptor Method

public class nw : DataService<NorthwindModel.NorthwindEntities>
{
     public static void InitializeService(IDataServiceConfiguration config)
     {
         config.SetEntitySetAccessRule("Orders", EntitySetRights.All);
     }

     [QueryInterceptor("Orders")]
     public Expression<Func<Orders,bool>> OnQueryOrders()
     {
         return o => o.Customer.ContactName == 
             HttpContext.Current.User.Identity.Name          
     }
}

Similarly, interceptors can also be added for inserts, updates, and deletes as change interceptors. Change interceptors have no return value and take two arguments, an object of the type contained in the EntitySet and an UpdateOperations enumeration that defines the action (update, insert, or delete) being requested on the resource. The interceptor may alter the object passed to it or set the reference to an entirely different instance. If the method throws an exception, the operation is aborted, no change is made in the database, and an error is returned to the client agent.

Client Side: Accessing the Service

In general, we have treated HTTP as the API for data services and thus have spent a lot of time ensuring that it is simple to use directly at the HTTP level. This means that not only is it easy to interpret service payloads, but that any platform with an HTTP stack can easily use data services. That said, if you are using a Microsoft platform to access data services, client-side development is further simplified using the ADO.NET Data Services Client Libraries. This presents a more natural programming model for applications written using the .NET Framework, Silverlight, and ASP.NET AJAX to target data services, manipulate the results in terms of objects, and handle aspects such as association traversal. The client libraries abstract away the details of HTTP requests and responses and also ensure a more uniform experience across each of the client stacks.

The .NET Framework and Silverlight client libraries consist of two primary types, the DataServiceContext class and the DataServiceQuery class. DataServiceContext represents the runtime context for a given data service. Data services themselves are stateless; however, the context in which the developer interacts is not, and state on the client is maintained between interactions in order to support features such as identity resolution and optimistic concurrency. The DataServiceQuery object represents a specific query against the store defined using the URI syntax. To execute a query and obtain the results in the form of .NET objects, you simply enumerate over the query object. You can do so using the standard "foreach" construct in C# or "For Each" in Visual Basic®, for example.

In order to use the entities defined in a data service as .NET objects on the client, corresponding classes need to be defined for the client application. There are three primary methods for accomplishing this. One option is to define them manually. Figure 5 shows a simple hand-written definition for the Region class and a short piece of code that executes a query against regions and prints their IDs and descriptions in the output.

Figure 5 Access an ADO.NET Data Service from a .NET App

namespace TestApplication
{
  public class Region
  {
     public int RegionID { get; set; }
     public string RegionDescription { get; set; }
  }

  class Program
  {
     static void Main(string[] args)
     {
        DataServiceContext ctx = new
           DataServiceContext("https://localhost:25115/NorthwindService.svc");

        DataServiceQuery<Region> regions =
            ctx.CreateQuery<Region>("/Region?$orderby=RegionID");

         foreach (Region r in regions)
         {
            Console.WriteLine(r.RegionID + ", " + r.RegionDescription);
         }
      }
   }
}

A more common approach is to use code generated by Visual Studio. While writing classes manually works well when the service has only a small number of types, as the data service schema grows more complex, the number and size of the classes that must be manually created and maintained also grows significantly.

The most common way to generate the necessary classes is by using the Add Service Reference wizard in Visual Studio. Similar to using Add Service Reference for a WCF service, simply right-click on the Project and choose Add Service Reference (see Figure 6).

fig06.gif

Figure 6 Add Service Reference

In the Add Service Reference dialog, enter the URI for the entry point of the service as the Address. In our example, we would enter <host>/<vdir>/NorthwindService.svc. This will generate classes based on the data service definition and add them to the project.

An alternate option is to use the "datasvcutil.exe" command-line tool to generate the classes based on the data service definition. This tool shipped with the release of ADO.NET Data Services and can be found in the directory \Windows\Microsoft.Net\Framework\V3.5. The tool takes as an argument the URI for the data service's entry point as well as the name and location of the output file that is to be generated, like so:

"\Windows\Microsoft.Net\Framework\V3.5\datasvcutil.exe" 
    /out:C:\NorthwindSample\northwind.cs /uri:"https://<host>/<vdir>/
    NorthwindService.svc"

The default output from the command-line tool is a C# file that contains a class for each of the entity types described in the data service (Visual Basic types can be generated using the /language:VB switch). Each generated class has members representing the primitive values and associations described in the service. This allows developers to navigate through a graph of associated entities using the object model directly.

.NET Client Library

The ADO.NET Data Services .NET client library presents a programming model that is familiar to developers who write apps using the .NET Framework and data services. Under the covers, the client library uses HTTP and the AtomPub format, so it works naturally over both corporate networks and Internet environments, requiring only simple HTTP-level connectivity to the data service, whether direct or indirect (for example, through proxies).

The client library can be used from any project type, including Windows Forms, Windows Presentation Foundation (WPF), and Web projects. In order to use the client library above, you will need to add a reference to the System.Data.Services.Client.dll assembly.

Silverlight Client Library

The Silverlight Client doesn't ship as a part of ADO.NET Data Services, but rather ships as part of the Silverlight 2 SDK, allowing for a more fully integrated experience when developing Silverlight apps. One difference between the Silverlight client library and the .NET and AJAX client libraries is that Silverlight 2 does not support synchronous development. Therefore, development with the Silverlight client library must make use of the asynchronous APIs, following the common begin/end async pattern. This also means that some of the APIs that you might use in the other two client libraries haven't been enabled in the Silverlight client library.

AJAX Client Library

The ASP.NET AJAX library is currently available at codeplex.com/aspnet/Release/ProjectReleases.aspx?ReleaseId=13357. Similar to the .NET client library discussed earlier, the AJAX client library abstracts the details of HTTP so that the application developer can work directly with JavaScript objects rather than manually parsing and creating HTTP requests and responses. A number of Web pages are also available on the CodePlex site that explain how to use the data service library for AJAX applications (see codeplex.com).

Querying a Data Service

In Figure 5, the queries against the service were built by calling the DataServiceQuery.CreateQuery method and passing in a URI query. As an alternative, the library also allows you to formulate data service queries using LINQ. The client library maps the LINQ statement to a URI in the target data service and retrieves the specified resources as .NET objects. The following code shows how to retrieve all the customers in the city of London with the results ordered by company name:

var q = from c in ctx.Customers
        where c.City == "London"
        orderby c.CompanyName
        select c;

foreach (var cust in q)
{
    Console.WriteLine(cust.CompanyName);
}

When using LINQ to ADO.NET Data Services, the set of queries expressible in the LINQ syntax is broader than those enabled by the REST-based URI syntax of Data Services. If a query cannot be mapped to a valid URI in the target data service, an exception will be thrown.

One way to understand the types of queries that can and cannot be mapped to URIs is to think about hierarchical traversal. Any query that requires two or more pivots, (for example, joins or subqueries that use clauses such as Any), cannot currently be mapped to a URI. Queries with a single pivot and association traversals, however, generally work well.

So far we have queried simple entities. Associations between objects are also tracked and managed by the DataServiceContext and associated objects can be loaded eagerly or as needed, using the URL formats discussed in the Uniform URI Formats section. To load associated entities on an as-needed basis (delay loading), use the LoadProperty method on the DataServiceContext class, as shown in Figure 7.

// get a single category
DataServiceQuery<Categories> categories =
               context.CreateQuery<Categories>("/Categories(1)");

foreach (Categories c in categories)
{
     Console.WriteLine(c.CategoryName);

     context.LoadProperty(c, "Products");

     foreach (Products p in c.Products)
     {
          Console.WriteLine("\t" + p.ProductName);
     }
}

In some scenarios, we may also want to avoid the extra round-trip on the wire to fetch the entities, preferring to load them along with the original query. In this case, the expand option is specified on the URI. The client library recognizes that the result includes both top-level and associated entities and will materialize all of them as a single object graph. Figure 8 eagerly loads related products in a single round-trip to the data service.

public IList<Products> GetProducts(string productName, 
    Categories category)
{
     int categoryId = category.CategoryID;
     string uri = serviceUri + "/Categories(" + categoryId + ")/Products?";
     if (!String.IsNullOrEmpty(productName)) 
     {
         uri = uri + "$filter=ProductName eq '" + productName + "'&";
     }
     uri = uri + "$expand=Categories";
     Uri queryUri = new Uri(uri);
     try
     {
         IEnumerable<Products> products = 
            context.Execute<Products>(queryUri);

         return products.ToList();
     }
     catch (Exception)
     {
         return null;
     }                  
}

Command Mapping

With queries through ADO.NET Data Services mapped through HTTP Get requests, how are Create, Update, and Delete requests executed? Each of the four CRUD operations (Create, Retrieve, Update, Delete) are mapped to a different HTTP verb with Retrieve mapped to GET, Create mapped to POST, Update mapped to PUT, and Delete mapped to DELETE. Just as with queries, the client library used (in this case, the .NET client library) will abstract away the details and use the appropriate HTTP verb, allowing the developer to use the provided APIs quite easily.

To create an instance of an entity in the data service, simply create the .NET object, populate it with the desired data, and then call AddObject on the DataServiceContext object, passing the new object and the name of the EntitySet that the object will be added to, as seen here:

public void AddProduct(Products product)
{
     context.AddObject("Products", product);
     context.AttachTo("Categories", product.Categories);
     context.SetLink(product, "Categories", product.Categories);
     DataServiceResponse r = context.SaveChanges();
}

It's also possible to add or change associations between .NET objects and have the client library reflect those changes as association creation/deletion operations. For example, the previous code creates a Product in the Northwind database and associates it with an existing Category. Categories and products participate in a one-to-many association; so, a given product has one specific category.

After an entity is created, the data service will return a fresh copy of it, including values that have been updated as a result of triggers in the database, autogenerated keys, and so on. The client library will then automatically update the .NET object with the new values.

To modify an existing entity instance, the object first needs to be fetched by the client and tracked by the DataServiceContext. The developer will first query for or attach the object to the context, make the desired changes to its properties, and then call the UpdateObject method. The UpdateObject method instructs the client library to send an update for that object:

public void UpdateProduct(Products product)
{
    Categories newCategory = product.Categories;
    context.AttachTo("Products", product);
    context.AttachTo("Categories", newCategory);
    context.UpdateObject(product);
    context.SetLink(product, "Categories", newCategory);

    context.SaveChanges();
}

To delete an entity instance, the object must again be held by the client and tracked by the DataServiceContext. Once tracked, we can make a simple call to the DeleteObject method on the context instance to mark the object for deletion:

public string DeleteProduct(Products product)
{
     context.AttachTo("Products", product);
     context.DeleteObject(product); 
     context.SaveChanges();
}

In all of the above examples, the last method call made on the context has been a call to the SaveChanges method. Changes are tracked in the DataServiceContext instance but not sent to the server immediately. Once all required changes for a given activity are complete, a call to SaveChanges will submit the changes to the data service.

So far, each of the examples has resulted in a single HTTP and sever request for each client operation. This approach (single operation per HTTP request) works well in some scenarios, but it is often a better idea to batch a number of operations and send them to the data service in a single HTTP request. This will reduce the number of round-trips to the data service and enable a logical scope of atomicity for collections of operations. To support these requirements, the client supports sending groups of CUD (Create, Update, Delete) operations and groups of Query operations to the data service in a single HTTP request. The following code shows how two or more queries can be sent to the data service as a single batch:

var q = (DataServiceRequest)from o in context.SalesOrder
                            select o;

// send two queries in one batch request to the data service
DataServiceResponse r = service.ExecuteBatch(
    new DataServiceRequest<Customer>(new 
      Uri("https://localhost:25115/NorthwindService.svc/Customers")),
      q);

Sending a group of CUD operations as a batch to the data service is accomplished using a simple call to the SaveChanges method and passing SaveChangesOptions.Batch as the only parameter. This parameter value instructs the client to batch all the pending change operations into a batch and send them to the data service as an atomic group. When sending change operations as a batch using SaveChangesOptions.Batch, either all changes will complete successfully or none of the changes will be applied.

Learning More

Although this article covers just enough to get you started with ADO.NET Data Services, there are still a number of topics that you may be interested in. For more information, go to the Data Services Team blog at blogs.msdn.com/astoriateam and on the MSDN Data Development center at msdn.microsoft.com/data under the topic heading ADO.NET Data Services.

Elisa Flasko is a Program Manager in the Data Programmability team at Microsoft, including the ADO.NET technologies, XML technologies, and SQL Server Connectivity technologies. She can be reached at blogs.msdn.com/elisaj.

Mike Flasko is a Program Manager in the SQL Data Programmability group at Microsoft. You can contact Mike through his blog at blogs.msdn.com/mflasko.