Using Microsoft ADO.NET Data Services
Microsoft Data Platform Development Technical Article
Mike Flasko
Microsoft® Corporation
Applies To:
- ADO.NET Data Services
- Visual Studio 2008 SP1
- .NET Framework 3.5 SP1
Summary: This document describes how to create and use Microsoft® ADO.NET Data Services, and discusses various details around the URI and payload formats. This document is meant as an introduction to ADO.NET Data Services and thus covers the core aspects of the technology, defering discussion of more advanced topics to companion documents.
Introduction:
The goal of Microsoft® ADO.NET Data Services is to enable applications to expose data as a data service that can be consumed by web clients within corporate networks and across the internet. A data service is reachable via regular HTTP requests, using standard HTTP verbs such as GET, POST, PUT and DELETE to perform CRUD operations against the service. The payload format used by the service is controllable by the application, but all options are simple, open formats such as JSON and Atom/APP.
The use of web-friendly technologies make ADO.NET Data Services ideal as a data back-end for AJAX-style applications, Rich Interactive Applications and other applications that need to operate against data that is stored across the web.
Getting Started: Creating Data Services
Pre-requisites
In order to create a data service using ADO.NET Data Services in your own environment you will need Microsoft Visual Studio 2008 SP1. If you will be using your data service to access data stored in a relational database, you will also need a database with updated data-access providers, such as Microsoft SQL Server 2005 or 2008 (any edition, including SQL Server Express, will work). Updated providers are also available for third party database such as Oracle and DB2. For more information on third party providers see https://msdn.microsoft.com/data.
The ADO.NET Entity Framework runtime and associated tools are included in Visual Studio 2008 SP1.
Selecting a Data Source
The ADO.NET Data Service server framework is comprised of two halves. The top-half is the runtime itself; this part is “fixed”, and it implements URI translation, the Atom/JSON wire formats, the interaction protocol, etc. This is what makes an ADO.NET Data Service look like an ADO.NET Data Service. The bottom half is the data-access layer and is pluggable. Communication between layers happens in terms of the IQueryable interface plus a set of conventions to map CLR graphs into the URI/payload patterns of ADO.NET Data Services.
The first step in creating an ADO.NET Data Service is to determine the data source that is to be exposed as a set of REST-based endpoints (ie. select or create a data access layer). For relational data stored in Microsoft SQL Server or other 3rd Party databases, ADO.NET Data Services currently enables easily exposing a conceptual model created using the ADO.NET Entity Framework (EF). For all other data sources (XML document, web service, application logic layer, etc) or to use additional database access technologies (ex. LINQ to SQL), a mechanism is provided which enables any data source, as per the plug-in model described above, to be exposed as an ADO.NET Data Service.
To create a data service which exposes a relational database through an Entity Framework conceptual model see “Creating a Data Service using the ADO.NET Entity Framework”. To create a data service which exposes another data source see “Creating a Data Service from any Data Source”.
Creating a Data Service using the ADO.NET Entity Framework
ADO.NET Data Services are a specialized form of Windows Communication Foundation services, and thus can be hosted in various environments. The below example will create an ADO.NET Data Service which is hosted inside an ASP.NET site. In order to create a data service, you must first create a web project; you will then need to establish a connection with the database that will be exposed by the service, and then create the data service itself within the web application. Below is a step-by-step description of this process.
**NOTE:**These steps are for Visual Studio Standard, Professional and Team System editions. If using Visual Studio Web Developer, create a new “Web Site” rather than a “Web Application”. The remaining workflow does not change. While this example uses a web application, other project types (websites) and hosting mechanisms are also supported.
- Create the project
- Create a “Web Application” project by going to the File menu in Visual Studio and choosing New Project. When the New Project window appears, choose either Visual Basic or Visual C# as the programming language. Within the language category click on “Web”, and select “ASP.NET Web Application” from the right-hand panel. Choose a name for the project, for example SimpleDataService, and click OK. NOTE: If you already have a web application and you’d like to add a new data service to it, you can skip step 1 and go directly to step 2.
- Create an Entity Data Model representation of your database using the ADO.NET Entity Framework
- Assuming that you already have a database that you want to expose as a data service, we will create an Entity Data Model schema that maps 1:1 with your database. Select SimpleDataService Add New Item in Visual Studio. The Add New Item window will appear, choose “ADO.NET Entity Data Model”, give it a name and click Add. We will use the Northwind sample database throughout this example, so we will use “Northwind” as our name (Northwind.edmx being the generated file name).
- In the rest of the step-by-step guide we will assume you are using the Northwind sample database, so the database connection created here should point to Northwind.NOTE:
- Create a data service
- To create the data service itself, select SimpleDataService Add New Item in Visual Studio. Choose “ADO.NET Data Service” from the list of item templates, give it a name (e.g. Northwind) and click add. NOTE: The template wizard offers both an ADO.NET Data Services option as well as a Web Service option.Select “ADO.NET Data Service”.
- Visual Studio will open the code file for the new service by default. You can also find the file in the Solution Explorer; it will have the name you indicated, with a “.svc.cs” or “.svc.vb” extension. In C# add a “using” clause at the beginning of the file to include the namespace of the model. In Visual Basic, this namespace is already imported at the project level automatically along with the project namespace. By default the model namespace is derived from the database that was used for the data service, unless changed when the EDM is created, for example if the database was called Northwind the namespace will be NorthwindModel.
- Locate the “TODO: put your data source class name here” comment that indicates to put in the class name that represents the database, and replace it with the name of the class that was generated by the Entity Data Model Wizard in step 2.b. Again, the name is derived from the database name, unless changed, so for example if the database is called “Northwind” the class will be “NorthwindEntities”.
- Enable access to the data service
- By default a data service does not expose any resources.For security purposes, access to resources needs to be explicitly enabled before any resources or associations are accessible. To enable read and write access to all resources in the Entity Data Model associated with the service; locate the InitializeService method as shown in Example 1 below.
The code file (e.g. northwind.svc.cs) should look more or less like the example below.
C#:
using System;
using System.Web;
using System.Collections.Generic;
using System.ServiceModel.Web;
using System.Linq;
using System.Data.Services;
using NorthwindModel;
namespace SimpleDataService
{
public class Northwind : DataService<NorthwindEntities>
{
public static void InitializeService(IDataServiceConfiguration
config)
{
config.SetEntitySetAccessRule("*", EntitySetRights.All);
}
}
}
VB:
Imports System.Data.Service
Imports System.Linq
Imports System.ServiceModel.Web
Public Class Northwind
Inherits DataService(Of NorthwindEntities)
Public Shared Sub InitializeService(ByVal config As IDataServiceConfiguration)
config.SetEntitySetAccessRule("*", EntitySetRights.All)
End Sub
End Class
Example 1: Basic data service in C# and VB
To test the data service, simply hit Ctrl+F5 within Visual Studio, which will start the development web server and will run the Data Services server inside it. See the “Trying an ADO.NET Data Service” section below to walk through a trial run of your newly created data service.
Creating a data service from any data source
In general, ADO.NET Data Services work by converting a request to perform an operation over a resource, identified by a URI, to the equivalent operation on the data model it represents. When the data model is backed by a relational database, URIs are converted to Entity Framework Object Services method calls (as per the section ‘Creating a local data service using the ADO.NET Entity Framework’ above). Since that approach is specific to the Entity Framework, a different method is required when the underlying data model is backed by a data source other than a relational database (XML Document, web service, etc). In this case, requests to URIs are converted to LINQ queries. To enable this approach, ADO.NET Data Services maps objects implementing the IQueryable interface in a CLR object graph to Entity Sets. This approach enables the ADO.NET Data Services Framework to expose any data source which has an IQueryable provider written for it. In order to enable such layering (ADO.NET Data Services over IQueryable-based data sources) a mapping is defined between CLR objects and artifacts of the EDM-based data model used by the ADO.NET Data Services Framework.
The remainder of this section defines by example the mapping between CLR constructs and artifacts of an EDM-based data model. How to define classes implementing the IQueryable interface such that they are represented as Entity Sets in the service is shown below; however, IQueryable implementation specifics will be covered in a separate document.
**NOTE:**The steps below walk through creating an ADO.NET Data Service based on a CLR object graph. These steps produce a data service created over an in memory collection of objects. This approach is done as a simple way to show how an arbitrary CLR graph can be exposed as a service and is handy for mocking data sources. In a typical production deployment, the IQueryable properties shown representing Entity Sets would not expose in memory data, but rather would translate the IQueryable expression trees to data source specific queries.
- Create the project
- Create a “Web Application” project by going to the File menu in Visual Studio and choosing New Project. When the New Project window appears, choose either Visual Basic or Visual C# as the programming language, and within the language category click on “Web”, and select “ASP.NET Web Application” from the right-hand panel. Choose a name for the project, for example CustomDataService, and click OK. NOTE: If you already have a web application and you’d like to add a new data service to it, you can skip step 1 and go directly to step 2.
- Create a data service
- To create the data service itself, select CustomDataService Add New Item in Visual Studio. Choose “ADO.NET Data Service” from the list of item templates, give it a name (e.g. contacts) and click add. NOTE:
- Copy & paste the code from Example 2 into the code file opened in step 2.b.
- In the CustomDataService namespace you should now have a class called ‘ContactsData’ which has two public properties (Contacts & Users) which return IQueryable<Contact> and IQueryable<User>.These properties represent Entity Sets in the CLR model.The User and Contact classes in turn define the base Entity Types for each of the Entity Sets.
- Enable access to the data service
- By default a data service does not expose any resources.For security purposes, access to resources needs to be explicitly enabled before any resources or associations are accessible. To enable read and write access to all resources in the Entity Data Model associated with the service, locate the InitializeService method and ensure it matches what is shown in Example 2 below.
- Try the data service
- Press F5 to run the project and navigate to /contacts.svc.This returns the root document for the service.Navigating to /contacts.svc/Users will return all users.
- See the ‘Trying an ADO.NET Data Service’ section below
C#:
using System;
using System.Collections.Generic;
using System.Data.Services;
using System.Linq;
namespace CustomDataService
{
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 ContactsData : //IUpdatable
{
#region Populate Service Data
static User[] _users;
static Contact[] _contacts;
static ContactsData()
{
_users = new User[]{
new User(){ ID=0, Name="Mike",Contacts= new List<Contact>()},
new User(){ ID=1, Name="Saaid",Contacts= new List<Contact>()},
new User(){ ID=2, Name="John",Contacts= new List<Contact>()},
new User(){ ID=3, Name="Pablo",Contacts= newList<Contact>()}};
_contacts = new Contact[]{
new Contact(){ ID=0, Name="Mike", Email="mike@contoso.com" },
new Contact(){ ID=1, Name="Saaid", Email="Saaid@hotmail.com"},
new Contact(){ ID=2, Name="John", Email="j123@live.com"},
new Contact(){ ID=3, Name="Pablo", Email="Pablo@mail.com"}};
_users[0].Contacts.Add(_contacts[0]);
_users[0].Contacts.Add(_contacts[1]);
_users[1].Contacts.Add(_contacts[2]);
_users[1].Contacts.Add(_contacts[3]);
}
#endregion
public IQueryable<User> Users
{
get { return _users.AsQueryable<User>(); }
}
public IQueryable<Contact> Contacts
{
get { throw new DataServiceException(403, "Requests directly to
/Contacts are not allowed");}
}
public class contacts : DataService<ContactsData>
{
// This method is called only once to initialize
//service-wide policies.
public static void InitializeService(IDataServiceConfiguration
config)
{
config.SetEntitySetAccessRule("*", EntitySetRights.All);
}
//Service operations, query interceptors & change interceptors go here
}
}
VB:
Imports System.Data.Services
Imports System.Linq
Imports System.ServiceModel.Web
Public Class User
Private _id As Integer
Public Property ID() As Integer
Get
Return _id
End Get
Set(ByVal value As Integer)
_id = value
End Set
End Property
Private _name As String
Public Property Name() As String
Get
Return _name
End Get
Set(ByVal value As String)
_name = value
End Set
End Property
Private _contacts As IList(Of Contact)
Public Property Contacts() As IList(Of Contact)
Get
Return _contacts
End Get
Set(ByVal value As IList(Of Contact))
_contacts = value
End Set
End Property
End Class
Public Class Contact
Private _id As Integer
Public Property ID() As Integer
Get
Return _id
End Get
Set(ByVal value As Integer)
_id = value
End Set
End Property
Private _name As String
Public Property Name() As String
Get
Return _name
End Get
Set(ByVal value As String)
_name = value
End Set
End Property
Private _email As String
Public Property Email() As String
Get
Return _email
End Get
Set(ByVal value As String)
_email = value
End Set
End Property
End Class
Public Class ContactsData
'Implements IUpdatable
#Region "-- Populate Service Data --"
Private Shared _users As User()
Private Shared _contacts As Contact()
Shared Sub New()
Dim users As User() = _
{New User With {.ID = 0, .Name = "Mike", .Contacts = _
New List(Of Contact)}, New User With {.ID = 1, .Name = _
"Saaid", .Contacts = New List(Of Contact)}, _
New User With {.ID = 2, .Name = "John", .Contacts = _
New List(Of Contact)}, New User With {.ID = 3, .Name = _
"Pablo", .Contacts = New List(Of Contact)}}
Dim contacts As Contact() = _
{New Contact With {.ID = 0, .Name = "Mike", .Email = _
"mike@contoso.com"}, New Contact With {.ID = 1, .Name = _
"Saaid", .Email = "Saaid@hotmail.com"}, _
New Contact With {.ID = 2, .Name = "John", .Email = _
"j123@live.com"}, New Contact With {.ID = 3, .Name = _
"Pablo", .Email = "Pablo@mail.com"}}
users(0).Contacts.Add(contacts(1))
users(1).Contacts.Add(contacts(2))
users(1).Contacts.Add(contacts(3))
_users = users
_contacts = contacts
End Sub
#End Region
Public ReadOnly Property Users() As IQueryable(Of User)
Get
Return _users.AsQueryable()
End Get
End Property
Public ReadOnly Property Contacts() As IQueryable(Of Contact)
Get
Throw New DataServiceException(403, _
"Requests directly to Contacts are not allowed")
End Get
End Property
End Class
Public Class Contacts
Inherits DataService(Of ContactsData)
' This method is called only once to initialize service-wide policies.
Public Shared Sub InitializeService(ByVal config As _
IDataServiceConfiguration)
config.SetEntitySetAccessRule("*", EntitySetRights.All)
End Sub
'Service operations, query interceptors & change interceptors go here
End Class
Example 2: ADO.NET Data Service exposing an in-memory data source
Trying an ADO.NET Data Service
The easiest way to test your ADO.NET Data Service is to simply access it with a web browser. While this is probably not the way you will ultimately use the data service (it is more likely that a program will interact with it), it is an easy way to understand how requests work, what results look like, and other details surrounding the implementation of the service.
To interact with the data service, open a web browser such as Microsoft Internet Explorer and point it to the URL that is the entry point to the site. If you created the data service locally using Visual Studio, you can simply hit Ctrl+F5 to start the web server, and then point the URL in the browser that Visual Studio launches to the data service file. For example, “http://host/vdir/northwind.svc” where “host” is a computer name or localhost; you may also need to add a port number.
**NOTE:**To view Atom (the default format returned by an ADO.NET Data Service) in Internet Explorer, you must first ensure that Feed Reading View is turned off. This can be done on the Content tab of Tools | Internet Options.
When you hit the entry point, (after setting the Entity Set access rules as per the examples above) the response is an XML response that contains the list of Entity Sets exposed by the data service. Since the default serialization used by a data service is Atom, the document returned by default is an Atom service document (as shown in the examples below). If you are using the full Northwind sample database, the output will be similar to what is shown in Example 3 below.
<?xml version="1.0" encoding="utf-8" standalone="yes" ?> <service xml:base="https://localhost:51905/nw.svc/" xmlns:atom=http://www.w3.org/2005/Atom xmlns:app="http://www.w3.org/2007/app" xmlns="http://www.w3.org/2007/app"> <workspace> <atom:title>Default</atom:title> <collection href="Categories"> <atom:title>Categories</atom:title> </collection> <collection href="CustomerDemographics"> <atom:title>CustomerDemographics</atom:title> </collection> <collection href="Customers"> <atom:title>Customers</atom:title> </collection> <collection href="Employees"> <atom:title>Employees</atom:title> </collection> <collection href="Order_Details"> <atom:title>Order_Details</atom:title> </collection> <collection href="Orders"> <atom:title>Orders</atom:title> </collection> <collection href="Products"> <atom:title>Products</atom:title> </collection> <collection href="Region"> <atom:title>Region</atom:title> </collection> <collection href="Shippers"> <atom:title>Shippers</atom:title> </collection> <collection href="Suppliers"> <atom:title>Suppliers</atom:title> </collection> <collection href="Territories"> <atom:title>Territories</atom:title> </collection> </workspace> </service>
Example 3: Response for the root of a data service
Using this as a starting point, you can start to browse the contents of the data service. To continue with the Northwind example, you can add “/Products” to the URL, resulting in “http://host/northwind.svc/Products”, which returns all of the products in the store. Example 4 below shows a partial listing of the result.
<?xml version="1.0" encoding="utf-8" standalone="yes" ?> <feed xml:base="http://host/northwind.svc/" xmlns:d="https://schemas.microsoft.com/ado/2007/08/dataservicesdataservices" xmlns:m="https://schemas.microsoft.com/ado/2007/08/dataservicesdataservices/metadata" xmlns="http://www.w3.org/2005/Atom"> <id>http://host/northwind.svc/Products</id> <updated /> <title type=”text”>Products</title> <link rel="self" href="Products" title="Products" /> <entry> <id>http://host/northwind.svc/Products(1)</id> <updated /> <title /> <author> <name /> </author> <link rel="edit" href="Products(1)" title="Products" /> <category term="NorthwindModel.Products" scheme="https://schemas.microsoft.com/ado/2007/08/dataservices/scheme" /> <content type="application/xml"> <m:properties> <d:ProductID m:type="Edm.Int32">1</d:ProductID> <d:ProductName>Chai</d:ProductName> <d:QuantityPerUnit>10 boxes x 20 bags</d:QuantityPerUnit> <d:UnitPrice m:type="Edm.Decimal">18.0000</d:UnitPrice> <d:UnitsInStock m:type="Edm.Int16">39</d:UnitsInStock> <d:UnitsOnOrder m:type="Edm.Int16">0</d:UnitsOnOrder> <d:ReorderLevel m:type="Edm.Int16">10</d:ReorderLevel> <d:Discontinued m:type="Edm.Boolean">false</d:Discontinued>
</m:properties> </content> <link rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Categories" title="Categories" href="Products(1)/Categories" type="application/atom+xml;type=entry" /> <link rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Order_Details" title="Order_Details" href="Products(1)/Order_Details" type="application/atom+xml;type=feed" /> <link rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Suppliers" title="Suppliers" href="Products(1)/Suppliers" type="application/atom+xml;type=entry" /> </entry> <entry> … </entry> </feed>
Example4: Listing of the contents of an entity set, in Atom/APP format
If you want to construct a URL that points to a particular entity, such as a particular Product in this example, you can do so by adding the key value in parenthesis (the URL of a given entity can be obtained by composing the name of the entity instance element, and the base URL of the XML document). For example, the URL “http://host/northwind.svc/Products(1)” would produce the results shown in Example 5 below.
<?xml version="1.0" encoding="utf-8" standalone="yes" ?> <entry xml:base="https://localhost:63952/northwind.svc/" xmlns:d="https://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="https://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom"> <id>https://localhost:63952/northwind.svc/Products(1)</id> <title type="text" /> <updated>2008-06-24T01:30:30Z</updated> <author> <name /> </author> <link rel="edit" title="Products" href="Products(1)" /> <link rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Categories" type="application/atom+xml;type=entry" title="Categories" href="Products(1)/Categories" /> <link rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Order_Details" type="application/atom+xml;type=feed" title="Order_Details" href="Products(1)/Order_Details" /> <link rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Suppliers" type="application/atom+xml;type=entry" title="Suppliers" href="Products(1)/Suppliers" /> <category term="NorthwindModel.Products" scheme="https://schemas.microsoft.com/ado/2007/08/dataservices/scheme" /> <content type="application/xml"> <m:properties> <d:ProductID m:type="Edm.Int32">1</d:ProductID> <d:ProductName>Chai</d:ProductName> <d:QuantityPerUnit>10 boxes x 20 bags</d:QuantityPerUnit> <d:UnitPrice m:type="Edm.Decimal">18.0000</d:UnitPrice> <d:UnitsInStock m:type="Edm.Int16">39</d:UnitsInStock> <d:UnitsOnOrder m:type="Edm.Int16">0</d:UnitsOnOrder> <d:ReorderLevel m:type="Edm.Int16">10</d:ReorderLevel> <d:Discontinued m:type="Edm.Boolean">false</d:Discontinued> </m:properties> </content> </entry>
Example 5: Response for a single-entity URL
If you look at the values of the properties on the entity in Example 5, you will notice that some of them such as ProductName and UnitPrice are scalar values, where as others such as Categories and Suppliers are different (they are part of a link element). The latter ones represent “navigation properties”, properties that navigate from an entity (Product in this case) to a related entity (the product’s category or its supplier in this case). URLs can be composed to traverse this relationship. For example, to see the supplier for this product we would use:
http://host/northwind.svc/Products(1)/Suppliers
In this section, we have gone through a quick overview of how to create and interact with data services. Please see the additional sections for a deeper look at ADO.NET Data Services.
Finding and Pointing to Data: URLs in Data Services
ADO.NET Data Services defines a simple, yet very expressive, URL format that allows applications to point to sets of entities and individual entities, as well as to traverse relationships between entities. Several options can also be added as query string parameters to control how the data is presented.
Structure of Web Data Services URLs
The basic format for URLs is:
http://host/<service>/<EntitySet>[(<Key>)[/<NavigationProperty>[(<Key>)/...]]]
**NOTE:**In the syntax above, [ ] imply optional components
The 3 main elements of the URL syntax are:
- **The data service URI.**The data service URI is the first part of the URL that points to the root of the data service.This will typically be (but is not limited to) a .svc file. For example, http://host/myapp/northwind.svc. The examples below assume that the URLs start with that prefix for brevity.
- **The entity-set name (optional).**If you include an entity-set name, then all the entities in that entity-set are returned. For example, /Customers would return all of the customers in the Northwind data service. The system also allows for an optional filter predicate contained in parenthesis to subset the response to a single entity.For single-key entities, you can simply indicate the key value, and the resulting URL will point to that entity specifically. For example, if there is a customer entity with a string-based key ‘ALFKI’, its URL would be /Customers(‘ALFKI’).Additional expression-based filtering on a set is enabled by using query string parameters, which are described later in this document
- **A navigation property (optional).**A navigation property can be placed after the entity-set name (separated by a “/”), indicating that you want to traverse the relationship being pointed to. For example, /Customers(‘ALFKI’)/Orders would return the sales orders of the customer with the primary key ‘ALFKI’. Similar to the entity set name, a filter can also be applied to the navigation property using query string operators (described later in this document) to return only a subset of the related entities. For example, /Customers(‘ALFKI’)/Orders?$filter=OrderDate gt '1998-1-1' returns all the orders posted after Jan 1st, 1998, for the customer with a key ‘ALFKI’. Since the result of traversing a relationship through a navigation property is another set of entities, you can continue to add navigation properties to the URL to navigate through the relationship graph specified in the data service schema. For example, /Customers(‘ALFKI’)/Orders(1)/Employees returns the employees that created sales order 1 for the customer with a key of ‘ALFKI’.
Query string options
While the URL format allows for filtering and traversing through the graph of entities in the store, it does not have constructs to control the output. For that, a number of optional query string parameters are supported by ADO.NET Data Services. Table 1 below lists all of the query options along with their description and some usage examples.
Option | Description | Example |
---|---|---|
expand |
The ‘expand’ option allows you to embed one or more sets of related entities in the results. For example, if you want to display a customer and its sales orders, you could execute two requests, one for /Customers(‘ALFKI’) and one for /Customers(‘ALFKI’)/Orders. The ‘expand’ option on the other hand allows you to return the related entities in-line with the response of the parent in a single HTTP request. You may specify multiple navigation properties to expand by separating them with commas, and you may traverse more than one relationship by using a dot to jump to the next navigation property. |
--a customer with related sales orders --a customer with related sales orders and employee information related to those orders --Orders with related employees information and related shipper information |
orderby |
Sort the results by the criteria given in this value. Multiple properties can be indicated by separating them with a comma. The sort order can be controlled by using the “asc” (default) and “desc” modifiers. |
/Customers?$orderby=City /Customers?$orderby=City desc /Customers?$orderby=City desc,CompanyName asc |
skip |
Skip the number of rows given in this parameter when returning results. This is useful in combination with “top” to implement paging (e.g. if using 10-entity pages, saying $skip=30&top=$10 would return the fourth page). NOTE: Skip only makes sense on sorted sets; if an orderby option is included, ‘skip’ will skip entities in the order given by that option. If no orderby option is given, ‘skip’ will sort the entities by primary key and then perform the skip operation. |
--return all customers except the first 10 /Customers?$skip=10 --return the 4th page, in 10-row pages /Customers?$skip=30&$top=10 |
top |
Restrict the maximum number of entities to be returned. This option is useful both by itself and in combination with skip, where it can be used to implement paging as discussed in the description of ‘skip’. |
--top 5 sales orders /Customers?$top=5 --top 5 sales orders with the highest TotalDue /Orders?$orderby=TotalDue&$top=5 |
filter |
Restrict the entities returned from a query by applying the expression specified in this operator to the entity set identified by the last segment of the URI path. |
-- all customers in London /Customers?$filter=City eq ‘London’ -- Match all Customers with the value of the property ‘fullname’ equal to ‘Wayne, John’ /Customers?$filter='Wayne, John' eq insert(ContactName, length(lastname), ',') |
Table 1: Query string options
Expression Syntax
The simple expression language that is used in filter operators (and also supported in orderby operations) supports references to columns and literals. The literal values can be strings enclosed in single quotes, numbers and boolean values (true or false) or any of the additional literal representations shown in the ‘Data Type Literal Representations’ section below. The operators in the expression language use abbreviations of the names rather than symbols to reduce the amount of escaping necessary in the URL. The abbreviations are listed in Table 2.
Operator | Description | Example |
---|---|---|
Logical Operators |
||
eq |
Equal |
/Customers?filter=City eq 'London' |
ne |
Not equal |
/Customers?filter=City ne 'London' |
gt |
Greater than |
/Product?$filter=UnitPrice gt 20 |
ge |
Greater than or equal |
/Orders?$filter=Freight ge 800 |
lt |
Less than |
/Orders?$filter=Freight lt 1 |
le |
Less than or equal |
/Product?$filter=UnitPrice le 20 |
and |
Logical and |
/Product?filter=UnitPrice lteq 20 and UnitPrice gt 10 |
or |
Logical or |
/Product?filter=UnitPrice lteq 20 or UnitPrice gt 10 |
not |
Logical negation |
/Orders?$ ?$filter=not endswith(ShipPostalCode,'100') |
Arithmetic Operators |
||
add |
Addition |
/Product?filter=UnitPrice add 5 gt 10 |
sub |
Subtraction |
/Product?filter=UnitPrice sub 5 gt 10 |
mul |
Multiplication |
/Orders?$filter=Freight mul 800 gt 2000 |
div |
Division |
/Orders?$filter=Freight div 10 eq 4 |
mod |
Modulo |
/Orders?$filter=Freight mod 10 eq 0 |
Grouping Operators |
||
( ) |
Precedence grouping |
/Product?filter=(UnitPrice sub 5) gt 10 |
Table 2: Operators for filter expressions
In addition to the operators described above, a set of functions are also defined for use with the filter query string operator. The following tables list the available functions. This release does not support Aggregate functions (sum, min, max, avg, etc) as they would change the meaning of the ‘/’ operator to allow traversal through sets. For example, /Customers?$filter=average(Orders/Amount) gt 50.00 is not supported. Additionally, ISNULL or COALESCE operators are not defined. Instead, there is a null literal which can be used for comparison following CLR semantics.
String Functions |
---|
bool substringof(string p0, string p1) |
bool endswith(string p0, string p1) |
bool startswith(string p0, string p1) |
int length(string p0) |
int indexof(string arg) |
string insert(string p0, int pos, string p1) |
string remove(string p0, int pos) |
string remove(string p0, int pos, int length) |
string replace(string p0, string find, string replace) |
string substring(string p0, int pos) |
string substring(string p0, int pos, int length) |
string tolower(string p0) |
string toupper(string p0) |
string trim(string p0) |
string concat(string p0, string p1) |
Table 3: String Functions
Date Functions |
---|
int day(DateTime p0) |
int hour(DateTime p0) |
int minute(DateTime p0) |
int month(DateTime p0) |
int second(DateTime p0) |
int year(DateTime p0) |
Table 4: Date Functions
Math Functions |
---|
double round(double p0) |
decimal round(decimal p0) |
double floor(double p0) |
decimal floor(decimal p0) |
double ceiling(double p0) |
decimal ceiling(decimal p0) |
Table 5: Math Functions
Type Functions |
---|
bool IsOf(type p0) |
bool IsOf(expression p0, type p1) |
<p0> Cast(type p0) |
<p1> Cast(expression p0, type p1) |
Table 5: Type Functions
Examples
- /Orders?$filter=ID eq 1From
- From all the Orders in the data store, return only the Orders with the ‘ID’ property equal to 201
- /Customers?$filter='Wayne, John' eq insert(fullname, length(lastname), ',')
- Match all Customers with the value of the property ‘fullname’ equal to ‘Wayne, John’
- /Customer$filter=isof(‘SpecialCustomer’)
- Match all customers that are of type SpecialCustomer.Entity sets support inheritance and thus customer entities in the entity set may be of different types within a type hierarchy
Data Type Literal Representations
Each of the supported data types in ADO.NET Data Services has a literal form defined. This literal form is used in URLs generated and accepted by the ADO.NET Data Services Framework to identify the data type of a literal. The literal form of each supported data type is shown in Table 6 below.
ADO.NET Data Services Literal Form | EDM Type | CLR Type |
---|---|---|
null |
null |
null |
binary‘[A-Fa-f0-9][A-Fa-f0-9]*’ X ‘[A-Fa-f0-9][A-Fa-f0-9]*’ NOTE: X is case sensitive and binary isn't. Spaces are not allowed between binary and the quoted portion. Spaces are not allowed between X and the quoted portion. Odd pairs of hex digits are not allowed. |
Edm.Binary |
byte[] |
true | false |
Edm.Boolean |
Bool |
[A-Fa-f0-9]+ |
Edm.Byte |
Byte |
datetime'yyyy-mm-ddThh:mm[:ss[.fffffff]]' NOTE: Spaces are not allowed between datetime and quoted portion. datetime is case-insensitive.The format actually allows anything from the DateTimeKind.RoundtripKind, which may preserve timezone information. |
Edm.DateTime |
DateTime |
decimal‘[0-9]’ |
Edm.Decimal |
Decimal |
‘[0-9]+ ([.[0-9]+] | [E[+ | -][0-9]+]) Allows 1E+10 |
Edm.Double |
Double |
‘[0-9]+.[0-9]+f’ |
Edm.Float |
float |
guid‘dddddddd-dddd-dddd-dddddddddddd' where each d represents [A-Fa-f0-9] NOTE: guid is case insensitive and spaces are not allowed between guid and the quoted portion. |
Edm.Guid |
Guid |
‘[0-9]+’ |
Edm.Int16 |
Int16 |
‘[0-9]+’ |
Edm.Int32 |
Int32 |
‘[0-9]+L’ |
Edm.Int64 |
Int64 |
‘[0-9]+’ |
Edm.SByte |
SByte |
‘[0-9]+F’ |
Edm.Single |
Single |
'char*' NOTE: Delimiters are escaped by doubling them. |
Edm.String |
String |
Table 6: Literal Forms for Supported Data Types
Options for Data Representation
ADO.NET Data Services currently supports exchanging entities in JSON and Atom (an XML- based feed format). In all cases, the same format can be used both to receive information from the data service as well as to send to it.
Which format is best depends largely on the nature of the application and the runtime environment being used. For example, AJAX-based applications that run inside web browsers may find the JSON format easier to use, as it can be easily turned into JavaScript objects. On the other hand, a client application may be written with a language/runtime library that has a built-in XML parser, making the Atom format would be a good choice.
The mechanism used to specify in which format information is sent to a data service is the “Content-Type” HTTP header. Following the HTTP RFC 2616, the mechanism used to control the format returned by the system is the “Accept” HTTP header.
Requested Mime Type | Response Mime Type | Serialization Format |
---|---|---|
Grouping Media Types |
||
*/* |
application/atom+xml |
AtomPub |
text/* |
Not supported |
N/A |
application/* |
Not supported |
N/A |
Individual Media Types |
||
text/xml |
text/xml |
AtomPub |
application/xml |
application/xml |
AtomPub |
application/atom+xml |
application/atom+xml |
AtomPub |
application/json |
application/json |
JSON |
Table 7: Supported payload formats
Atom format details
The Atom Publishing Protocol is an HTTP-based approach for creating and editing Web resources. It is designed fundamentally around the idea of using the basic operations provided by the HTTP protocol (such as GET, PUT, POST and DELETE) to pass around instances of Atom 1.0 Feed and Entry documents that represent things like blog entries, podcasts, wiki pages, calendar entries and so on.
To enable Atom serialization in a data service, a fixed mapping from entities to Atom constructs is defined. For responses from the data service, the response can be either a set or a single entity. Entity sets are represented as <atom:feed> elements and single entities as <atom:entry> elements. For requests to the data service, see the section “Changing Data in Web Data Services” below.
Each property of an Entity is represented by an element within the <atom:content> element. The <atom:content> element is a child element of <atom:entry>. The value of the property is included as the content of the associated element. Each <atom:entry> element has an <atom:ID> element that contains the URL to the entity. URLs are given in absolute form as per the Atom specification. Each response also includes an “xml:base” attribute that provides the base to resolve the relative URLs within the document. A hypothetical Customer entity for /Customers(‘ALFKI’) with no relationships is shown in Example 6.
<?xml version="1.0" ?>
<entry xmlns="http://www.w3.org/2005/Atom"
xml:base=https://server/service.svc
xmlns:d="https://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m="https://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
<title/>
<id>https://server/service.svc/Customer(’ALFKI’)</id>
<updated/>
<category term="NorthwindModel.Customers"
scheme="https://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
<content type=”application/xml”>
<m:properties>
<d:CustomerID>ALFKI</d:CustomerID>
<d:CompanyName> Alfreds Futterkiste</d:CompanyName>
<d:Address> Obere Str. 57</d:Address>
</m:properties>
</content>
</entry>
Example 6: A single-entity response from the data service.
If the URL specified in the request matches more than one entity, the entities are returned inside an <atom:feed> element, as illustrated by the partial result listing for /Customers in Example 7 below.
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<feed xml:base="https://server/service.svc/" xmlns:d="https://schemas.microsoft.com/ado/2007/08/dataservicesdataservices" xmlns:m="https://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom"> <id>https://server/service.svc/Customers</id> <updated /> <title>Customers</title> <link rel="self" href="Customers" title="Customers" /> <entry> <id>https://server/service.svc/Customers('ALFKI')</id> <updated /> <title /> <author> <name /> </author> <category term="NorthwindModel.Customers" scheme=https://schemas.microsoft.com/ado/2007/08/dataservices/scheme /> <link rel="edit" href="Customers('ALFKI')" title="Customers" /> <content type="application/xml"> <m:properties> <d:CustomerID>ALFKI</d:CustomerID> <d:CompanyName>Alfreds Futterkiste</d:CompanyName> <d:ContactName>Maria Anders</d:ContactName> <d:ContactTitle>Sales Representative</d:ContactTitle> <d:Address>Obere Str. 57</d:Address> <d:City>Berlin</d:City> <d:Region d:null="true" /> <d:PostalCode>12209</d:PostalCode> <d:Country>Germany</d:Country> <d:Phone>030-0074321</d:Phone> <d:Fax>030-0076545</d:Fax> <m:properties> </content> <link rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Orders" title="Orders" href="Customers('ALFKI')/Orders" type="application/atom+xml;type=feed" /> <link rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/CustomerDemographics" title="CustomerDemographics" href="Customers('ALFKI')/CustomerDemographics" type="application/atom+xml;type=feed" /> </entry> // Additional <entry> elements </feed>
Example 7: A response that contains multiple entities.
For entities that have relationships, a navigation property is included and represented using an <atom:link rel=”https://schemas.microsoft.com/ado/2007/08/dataservices/related/[NavProp]”> element. This element contains a URL pointing to the related entities (the related entities themselves are not included), as shown in Example 7 (Note the “Orders” navigation property).
If the request includes an option to expand one or more navigation properties, such as /Customers(‘ALFKI’)?$expand=Orders, the related entities are nested under the <atom:link rel=”related”> element representing the navigation property, as shown below in Example 8.
<?xml version="1.0" encoding="utf-8" standalone="yes" ?> <?xml version="1.0" encoding="utf-8" standalone="yes" ?> <entry xml:base="https://localhost:63952/northwind.svc/" xmlns:d="https://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m=https://schemas.microsoft.com/ado/2007/08/dataservices/metadata xmlns="http://www.w3.org/2005/Atom"> <id>https://localhost:63952/northwind.svc/Customers('ALFKI')</id> <title type="text" /> <updated>2008-06-24T02:08:05Z</updated> <author> <name /> </author> <link rel="edit" title="Customers" href="Customers('ALFKI')" /> <link rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Orders" type="application/atom+xml;type=feed" title="Orders" href="Customers('ALFKI')/Orders"> <m:inline> <feed> <title type="text">Orders</title> <id>https://localhost:63952/northwind.svc/Customers('ALFKI')/Orders</id> <updated>2008-06-24T02:08:05Z</updated> <link rel="self" title="Orders" href="Customers('ALFKI')/Orders" /> <entry> <id>https://localhost:63952/northwind.svc/Orders(10643)</id> <title type="text" /> <updated>2008-06-24T02:08:05Z</updated> <author> <name /> </author> <link rel="edit" title="Orders" href="Orders(10643)" /> <link rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Customers" type="application/atom+xml;type=entry" title="Customers" href="Orders(10643)/Customers" /> <link rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Employees" type="application/atom+xml;type=entry" title="Employees" href="Orders(10643)/Employees" /> <link rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Order_Details" type="application/atom+xml;type=feed" title="Order_Details" href="Orders(10643)/Order_Details" /> <link rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Shippers" type="application/atom+xml;type=entry" title="Shippers" href="Orders(10643)/Shippers" /> <category term="NorthwindModel.Orders" scheme="https://schemas.microsoft.com/ado/2007/08/dataservices/scheme" /> <content type="application/xml"> <m:properties> <d:OrderID m:type="Edm.Int32">10643</d:OrderID> <d:OrderDate m:type="Edm.DateTime">1997-08- 25T00:00:00</d:OrderDate> <d:RequiredDate m:type="Edm.DateTime">1997-09-22T00:00:00</d:RequiredDate> <d:ShippedDate m:type="Edm.DateTime">1997-09- 02T00:00:00</d:ShippedDate> <d:Freight m:type="Edm.Decimal">29.4600</d:Freight> <d:ShipName>Alfreds Futterkiste</d:ShipName> <d:ShipAddress>Obere Str. 57</d:ShipAddress> <d:ShipCity>Berlin</d:ShipCity> <d:ShipRegion m:null="true" /> <d:ShipPostalCode>12209</d:ShipPostalCode> <d:ShipCountry>Germany</d:ShipCountry> </m:properties> </content> </entry> //Additional <entry> elements </feed> </inline> </link> <category term="NorthwindModel.Customers" scheme="https://schemas.microsoft.com/ado/2007/08/dataservices/scheme" /> <content type="application/xml"> <m:properties> <d:CustomerID>ALFKI</d:CustomerID> <d:CompanyName>Alfreds Futterkiste</d:CompanyName> <d:ContactName>Maria Anders</d:ContactName> <d:ContactTitle>Sales Representative</d:ContactTitle> <d:Address>Obere Str. 57</d:Address> <d:City>Berlin</d:City> <d:Region m:null="true" /> <d:PostalCode>12209</d:PostalCode> <d:Country>Germany</d:Country> <d:Phone>030-0074321</d:Phone> <d:Fax>030-0076545</d:Fax> </m:properties> </content> </entry>
Example 8: Response with nested related entities using the "expand" option.
In addition to the regular format for exchanging entities and the per service metadata description available from each data service, Atom defines the concept of a service document which describes all the sets available from the service. ADO.NET Data Services will expose a service document from the URI representing the root of the service (ex. https://server/service.svc). The service document lists the URIs representing each of the entity sets available from the data service.
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<service xml:base="https://server/service.svc/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app" xmlns="http://www.w3.org/2007/app"> <workspace> <atom:title>Default</atom:title> <collection href="Categories"> <atom:title>Categories</atom:title> </collection> <collection href="CustomerDemographics"> <atom:title>CustomerDemographics</atom:title> </collection> <collection href="Customers"> <atom:title>Customers</atom:title> </collection> <collection href="Employees"> <atom:title>Employees</atom:title> </collection> <collection href="Order_Details"> <atom:title>Order_Details</atom:title> </collection> <collection href="Orders"> <atom:title>Orders</atom:title> </collection> <collection href="Products"> <atom:title>Products</atom:title> </collection> <collection href="Region"> <atom:title>Region</atom:title> </collection> <collection href="Shippers"> <atom:title>Shippers</atom:title> </collection> <collection href="Suppliers"> <atom:title>Suppliers</atom:title> </collection> <collection href="Territories"> <atom:title>Territories</atom:title> </collection> </workspace> </service>
Example9: Atom service document as returned from an ADO.NET Data Service.
JSON format details
The JSON format follows the data encoding described in the RFC 4627 to represent data using a subset of the JavaScript syntax. For more information on JSON see RFC 4627 in the IETF web site (http://tools.ietf.org/html/rfc4627) and the http://json.org web site.
The JSON format in ADO.NET Data Services also uses a fixed mapping from entities. Entities themselves are mapped to a JSON object, where each property of the object represents a property of the entity. For requests, a single object is expected by the data service. For responses, arrays are returned when sets are retrieved and a single JSON object when sets are filtered by key. To guard against Cross-Site Request Fogery (CSRF) attacks using JSON, the array or JSON object in a response is always wrapped by a single JSON object with a single member named “d” whose value includes the array or object representing the entity or entities being returned.
In addition to the set of properties that correspond to the entity, each JSON object also includes a member called “__metadata” (double underscore prefix). This member carries metadata for each entity exchanged between the client and server. The members of the metadata object are listed in Table 6.
Member | Description | Applies To |
---|---|---|
Type |
The name of the entity type |
All entities, both top-level and nested under another entity (through the use of the “expand” option). It is not present on navigation properties that have not been expanded. |
Uri |
The URI (relative to base URI of the target data service) for this entity in the data service |
Both entities and navigation properties. When a navigation property is not expanded, the value of the property includes a single “__deferred” (double underscore prefix) member which includes a Uri property which contains the URL that can be used to retrieve the related entities. |
Table 8: Members of the "__metadata" object in JSON format
Example 10 shows the response from a data service, over the Customers entity-set in the Northwind database, for a URL that requests a single entity back (/Customers(‘ALFKI’)).
{
"d" : { __metadata: {
uri: "https://server/service.svc/Customers(\'ALFKI\')",
type: "NorthwindModel.Customers" },
CustomerID: "ALFKI",
CompanyName: "Alfreds Futterkiste",
ContactName: "Maria Anders",
ContactTitle: "Sales Representative",
Address: "Obere Str. 57",
City: "Berlin",
Region: null,
PostalCode: "12209",
Country: "Germany",
Phone: "030-0074321",
Fax: "030-0076545",
Orders: { __deferred: { uri: "https://server/service.svc/Customers(\'ALFKI\')/Orders" } },
CustomerDemographics: { __deferred: { uri: “https://server/service.svc/Customers(\'ALFKI\')/CustomerDemographics" } } }
}
Example10: JSON response from a data service for a single 'Customer' entity
If more than one entity matches the criteria, the response from the data service is similar to the single-entity case shown in Example 10 above, except that an array of JSON objects is returned instead of a single object.
If the entity has navigation properties, such as Orders in Example 10, by default the value of the property is the __deferred object, which contains the URI that can be used to retrieve the related entities.
If a hierarchy is returned by the data service as a result of using the “expand” option, objects are nested in the JSON payload. This results in the proper references being created between JavaScript objects during de-serialization by the consumer of the data. Example 11 shows a Customer entity with expanded sales orders.
{
"d" : { __metadata: {
uri: "https://server/service.svc/Customers(\'ALFKI\')",
type: "NorthwindModel.Customers" },
CustomerID: "ALFKI",
CompanyName: "Alfreds Futterkiste",
ContactName: "Maria Anders",
ContactTitle: "Sales Representative",
Address: "Obere Str. 57",
City: "Berlin",
Region: null,
PostalCode: "12209",
Country: "Germany",
Phone: "030-0074321",
Fax: "030-0076545",
Orders: [ {
__metadata: { uri: "https://server/service.svc/Orders(10643)",
type: "NorthwindModel.Orders" },
OrderID: 10643,
OrderDate: "\/Date(872467200000)\/",
RequiredDate: "\/Date(874886400000)\/",
ShippedDate: "\/Date(873158400000)\/",
Freight: "29.4600",
ShipName: "Alfreds Futterkiste",
ShipAddress: "Obere Str. 57",
ShipCity: "Berlin",
ShipRegion: null,
ShipPostalCode: "12209",
ShipCountry: "Germany",
Customers: { __deferred: { uri: "https://server/service.svc/Orders(10643)/Customers" } },
Employees: { __deferred: { uri: "https://server/service.svc/Orders(10643)/Employees" } },
Order_Details: { __deferred: { uri: "https://server/service.svc/Orders(10643)/Order_Details" } },
Shippers: { __deferred: { uri: "https://localhost:51905/nw.svc/Orders(10643)/Shippers" } } },
// Additional order objects
]
CustomerDemographics: { __deferred: { uri: “https://server/service.svc/Customers(\'ALFKI\')/CustomerDemographics" } } }
}
Example11: A hierarchical result containing a Customer and its related Sales Orders, in JSON format.
Changing Data in ADO.NET Data Services
The data services exposed by ADO.NET Data Services not only allow you to obtain data, but also to change data in the store. Data services provide a uniform mechanism for creating, updating and deleting entities, as well as creating and deleting associations between entities.
Creating a new entity
To create a new entity, an HTTP POST request needs to be sent to the URI that points to the container for that entity. For example, for a data service created over the Northwind sample database in SQL Server, to create a new Category object you would send an HTTP POST request to the /Categories URI (e.g. http://host/vdir/northwind.svc/Categories).
The payload of the HTTP POST request is the entity data, encoded in any of the supported formats. The “Content-Type” header in the HTTP request needs to indicate the format so the data service knows how to interpret the data appropriately. This behavior for POST requests maps to that described in the AtomPub RFC. Not all the properties of a given entity type need to be included; those not included will take their default value in the server. If a given property is not nullable the create operation will fail. Example 12 shows the payload of a POST request to create a new category, using both the Atom and JSON formats.
ATOM:
<entry xmlns:d="https://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m="https://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
xmlns="http://www.w3.org/2005/Atom">
<content type="application/xml">
<m:properties>
<d:CategoryName>New Category</d:CategoryName>
</m:properties>
</content>
</entry>
JSON:
{
CategoryName: "New Category"
}
Example 12: Payloads for creating a new Category entity using Atom and JSON
In the case of Example 12, the primary key for the entity (CategoryID property) is declared in the model to be automatically generated by the server (an IDENTITY column in SQL Server), so there is no need to specify it here. If the key property (or properties) is not generated by the server then they must also be specified in the payload.
After processing the POST request, the data service will return the entity that was created, with all of its values updated to reflect any changes that happened on the server, such as server-generated keys, properties modified at the database level using triggers, etc. In addition to the entity returned in the payload, a ‘Location’ HTTP response header is returned that includes the URI to the newly created entity as per AtomPub RFC 5023.
ATOM:
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<entry xml:base="http://host/ northwind.svc/" xmlns:d="https://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="https://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom">
<id>http://host/northwind.svc/Categories(11)</id>
<updated />
<title />
<author>
<name />
</author>
<link rel="edit" href="Categories(11)" title="Categories" />
<category term="NorthwindModel.Categories"
scheme="https://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
<content type="application/xml">
<m:properties>
<d:CategoryID m:type="Int32">11</d:CategoryID>
<d:CategoryName>NewCat2</d:CategoryName>
<d:Description d:null="true" />
<d:Picture m:type="Byte[]" d:null="true" />
</m:properties>
</content>
<link
rel="
https://schemas.microsoft.com/ado/2007/08/dataservices/related/Products"
title="Products" href="Categories(11)/Products"
type="application/atom+xml;type=feed" />
</entry>
JSON:
{
"d" : {
__metadata: {
uri: "http://host/northwind.svc/Categories(11)",
type: "NorthwindModel.Categories"
},
CategoryID: 11,
CategoryName: "New Category",
Description: null,
Picture: null,
Products: {
__deferred: {
uri: "http://host/northwind.svc/Categories(11)/Products"
}
}
}
}
Example 13: Response from the data service after processing a POST request for creating a Category, in Atom and JSON formats
Updating existing entities using merge semantics
There are two possible options for updating existing entities, a merge-based update or a replace-based update. An existing entity can be modified (updated) by sending an HTTP MERGE request to the URI where the entity is located. Following with the example for creating an entity, to modify the Category entity with key ‘11’ from the Northwind data service you would use the /Categories(11) URI.
In the case of merge-based updates, the payload needs to be an entity and only needs to contain the properties that are being modified. If a property is not included, the value that is currently present in the server will be preserved. Example 14 shows the payload used to update the category that was inserted in the previous example.
ATOM:
<entry xmlns:d="https://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m="https://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
xmlns="http://www.w3.org/2005/Atom">
<content type="application/xml">
<m:properties>
<d:CategoryName>Changed Category</d:CategoryName>
</m:properties>
</content>
</entry>
JSON:
{
CategoryName: "Changed Category"
}
Example14: Payload used to modify an existing Category entity through an HTTP PUT request, Atom and JSON formats
The response from HTTP MERGE requests is a 204 (No Content) HTTP response. In future versions, we may consider enabling responses similar to that returned from POST requests where the response payload is the latest version of the entity.
Updating existing entities using replace semantics
An existing entity can be modified (updated) by sending an HTTP PUT request to the URL where the entity is located. Following with the example for creating an entity using a POST, to modify the Category entity with key ‘11’ from the Northwind data service you would use the /Categories(11) URI.
In the case of replace-based updates, the payload needs to be an entity and should contain all the properties of the entity (not including navigation properties). If a property is not included, the value is reset on the server to the default value for the property. This behavior for PUT requests maps to that described in the AtomPub RFC 5023. Example 13 shows the payload used to update the category that was inserted in the previous insert example. Since not all the properties are included in the payload, those not specified will be reset to their default values by the data service.
ATOM:
<entry xmlns:d="https://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m="https://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
xmlns="http://www.w3.org/2005/Atom">
<content type="application/xml">
<m:properties>
<d:CategoryName>Changed Category</d:CategoryName>
</m:properties>
</content>
</entry>
JSON:
{
CategoryName: "Changed Category"
}
Example 15: Payload used to modify an existing Category entity through an HTTP PUT request, Atom and JSON formats
The response from an HTTP PUT request is a 204 (No Content) HTTP response. In future, we may consider enabling responses similar to that returned from POST requests where the response payload is the latest version of the entity.
Deleting entities
Entities are deleted by executing an HTTP DELETE request against the URL that identifies the entity in the data service. No payload is necessary for a delete operation.
The data service response will not contain a payload. If the entity was deleted successfully then the request will be answered as success (HTTP status 204 (No Content)), without further details.
Note that if there are constraints at the database or Entity Data Model level that prevent an entity from being deleted, the delete will fail and an error (4xx or 5xx return code) will be returned to the caller.
Associating entities through relationships
The ADO.NET Data Services framework operates on a data model called the Entity Data Model (EDM). The EDM consists of the high-level constructs “entities” and “associations”. In the EDM, there is no need to use foreign keys, instead associations are a first-class construct and can be explicitly defined and then created/deleted to associate/disassociate entities.
There are two ways to manage associations between entities through the data services interface. The first is for association ends with cardinality of 1 or 0..1 (an optional relationship), and the second for association ends with cardinality “many”; the latter applies to “many” ends in both the 1-to-many and many-to-many cases.
For 1-end associations, the payload that is used for creating an entity can contain a pointer to the related entity, in the form of a URI. For example, in the Northwind sample that is used throughout this document there is a Regions entity-set and a Territories entity-set. All territories must have a Region (1-to-many relationship), so it is not possible to create a Territory entity without specifying to which Region entity it is bound to. Example 15 shows the payload used to create a Territory (using HTTP POST against /Territories) that points to an existing Region entity.
ATOM:
<entry xmlns:d="https://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m="https://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
xmlns="http://www.w3.org/2005/Atom"
xml:base="http://host/ vdir/northwind.svc/">
<content type="application/xml">
<m:properties>
<d:TerritoryID>9999</d:TerritoryID>
<d:TerritoryDescription>Test Territory</d:TerritoryDescription>
</m:properties>
</content>
<link rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Region" title="Region" href="Region(4)">
</link>
</entry>
JSON:
{
TerritoryID: '9999',
TerritoryDescription: 'Test Territory',
Region: {
__metadata: { uri: 'Region(4)' }
}
}
Example16: Payload to create a new Territory entity that includes an association to a Region entity.
After an entity has been created, it may be necessary to change the 1-end of the association to point to a different entity (or to set it in the first place if the relationship was optional and wasn’t set at creation time). In general, associations between entities can be addressed (just like entities themselves) by using the $links construct in the URI. For example, the URI Territories(‘9999’)/$links represents the collection of all links from the territory entity to another entity. The path segment following the $links segment (ex. Territories(‘9999’)/$links/Region) specifies the specific link/association you want to work with.
Example 17 uses $links to point an existing Territory entity to a different Region (using a HTTP PUT request to /Territories(‘9999’)/$links/Region; note that since it is an update operation to an association (navigation property), the payload only includes information for the association, the rest of the properties were not included.
**NOTE:**The request body must have a content type of application/xml or application/json. Application/atom+xml is not used as addressing associations is not included in the AtomPub protocol and thus it would be incorrect to state the representation below was ATOM.
ATOM:
<uri xmlns="https://schemas.microsoft.com/ado/2007/08/dataservices">http://service/northwind.svc/Region(4)</uri>
JSON:
{"uri": "http://service/northwind.svc/Region(4)"}
Example 17: Payload to update a Territory so it is associated with a different Region entity.
When the association end that needs to be changed has cardinality > 1, the $links URI construct is also used; however the payload may be a single URI or a list of URIs.
To associate an entity to another entity through a many-end, the client should do an HTTP POST to the URL representing the association between entities, with a payload that contains a collection of URLs identifying the entities to bind to the single entity. To remove the association, the client should do an HTTP DELETE against the URL that addresses the specific association that should be removed.
Continuing with the Northwind sample, the Territory and Employee entities have a many-to-many association between them. Example 18 shows how to associate a Territory with an Employee through this many-to-many association by sending an HTTP POST request to /Territories(‘9999’)/$links/Employees. To delete that association, a client would send an HTTP DELETE request to the URL /Territories(‘9999’)/$links/Employees(3), without a request payload.
ATOM:
<uri xmlns="https://schemas.microsoft.com/ado/2007/08/dataservices"> http://service/northwind.svc/Employees(2)</uri>
JSON:
{ "uri": " http://service/northwind.svc/Employees(2)" }
Example18: Keys-only payload format used for inserting an association
Deep Insert Operations
In addition to inserting a single entity per request, ADO.NET Data Services supports accepting a graph of entities in a request payload to insert within a single request. Continuing with the Northwind theme, example 19 shows how to insert a new territory entity associated to new employee entities and an existing region entity in a single request by sending a POST request to the URI /Territories.
ATOM:
<entry xmlns:d="https://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m="https://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
xmlns="http://www.w3.org/2005/Atom"
xml:base="http://host/northwind.svc/">
<content type="application/xml">
<m:properties>
<d:TerritoryID>7777</d:TerritoryID>
<d:TerritoryDescription>New Territory</d:TerritoryDescription>
</m:properties>
</content>
<link rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Region" title="Region" href="Region(4)">
</link>
<link rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Employees" title="Employees">
<m:inline>
<feed>
<entry>
<content type="application/xml">
<m:properties>
<d:EmployeeID>100</d:EmployeeID>
<d:LastName>Smith</d:LastName>
<d:FirstName>Joe</d:FirstName>
</m:properties>
</content>
</entry>
<entry>
<content type="application/xml">
<m:properties>
<d:EmployeeID>101</d:EmployeeID>
<d:LastName>Jones</d:LastName>
<d:FirstName>John</d:FirstName>
</m:properties>
</content>
</entry>
</feed>
</m:inline>
</link>
</entry>
JSON:
{
"TerritoryID": "7777",
"TerritoryDescription": "New Territory",
"Region": { "__metadata": {"uri": "Region(4)"} },
"Employees":[
{"EmployeeID": 100,
"LastName":"Smith",
"FirstName": "Joe"},
{"EmployeeID": 101,
"LastName":"Jones",
"FirstName": "John"}
]
}
Example 19: Inserting a graph of data in a single request
Optimistic concurrency
ADO.NET Data Services support optimistic concurrency to enable detection of update conflicts by leveraging HTTP/1.1 features including persistent caches, various if-* precondition headers and the ETag response header. An ETag (entity tag) is an HTTP response header returned by an HTTP/1.1 compliant web server used to determine change in content at a given URL. The value of the header is an opaque string representing the state of the resource.
The first step to enabling optimistic concurrency checks for a data service is to specify the properties of each Entity Type which together make up the concurrency token for the type. If the data service is created using the Entity Framework, this is done (as shown in Figure 1) by setting the “Concurrency Mode” attribute on properties of an Entity Type in the Visual Studio designer to “Fixed” instead of their default value of none. If the data service is created using a custom data source then the System.Data.Services.ETagAttribute is used to specify the concurrency tokens for the data source classes which represent entities. Doing this causes all queries for the Entity Type to return an ETag response header which carries the concurrency token information.
Figure 1: Setting Concurrency Mode on an Entity Type
For example, querying a data service which exposes data from the northwind database would result in Example 20 if the CategoryName property of the Category type was marked as the only property that makes up the types concurrency token:
Request:
GET nw.svc/Categories(11)
Host: http://myservice
Response:
HTTP/1.1 200 OK
ETag: W/"'Test%20Cat'"
Content-Type: application/atom+xml;charset=utf-8
Content-Length: 1145
Connection: Close
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<entry xml:base="http://myservice/nw.svc/" xmlns:d="https://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="https://schemas.microsoft.com/ado/2007/08/dataservices/metadata" m:etag="W/"'Test%20Cat'"" xmlns="http://www.w3.org/2005/Atom">
<id>http://myservice/nw.svc/Categories(11)</id>
<title type="text"></title>
<updated>2008-08-06T22:24:05Z</updated>
<author>
<name />
</author>
<link rel="edit" title="Categories" href="Categories(11)" />
<link rel="https://schemas.microsoft.com/ado/2007/08/dataservices/related/Products" type="application/atom+xml;type=feed" title="Products" href="Categories(11)/Products" />
<category term="NorthwindModel.Categories" scheme="https://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
<content type="application/xml">
<m:properties>
<d:CategoryID m:type="Edm.Int32">11</d:CategoryID>
<d:CategoryName>Test Cat</d:CategoryName>
<d:Description m:null="true" />
<d:Picture m:type="Edm.Binary" m:null="true" />
</m:properties>
</content>
</entry>
Example 20: Request and Response using CategoryName as a concurrency token
To perform a conditional update to the category queried above, a PUT (or MERGE) request is sent along with the ETag header value received from the prior query operation. For example, to update category 11’s name from “Test Cat” to “Updated Cat” then the request in Example 21 would be:
Request:
PUT nw.svc/Categories(11)
Host: http://myservice
Accept: application/json
Content-Type: application/json
If-Match: W/"Test%20Cat"
Content-Length: 33
{"CategoryName": "Updated Cat"}
Response:
HTTP/1.1 204 No Content
DataServiceVersion: 1.0;
ETag: W/"'Updated%20Cat'"
Content-Length: 0
…
Example 21: Request to update the name of a Category
Custom Behaviors on Data Services
Service Operations
Having all data in a given data service available to all users, with full access, is not appropriate for all scenarios. Although convenient in certain scenarios, in many cases, applications will need to make use of validation rules, means of restricting the set of visible entities and means of imposing policies to govern the way client agents interact with data.
In order to address the need to run custom logic on the data service, ADO.NET Data Services support service operations. Data service operations are an extension to the Windows Communication Foundation (WCF) infrastructure; WCF allows developers to expose .NET methods as service operations using both SOAP and REST style interfaces. The Data Service extensions work in conjunction with the rest of the data service to maintain the flexibility of the service even when using methods.
These service operations take the form of methods added to the Data Service-derived type that represents the data service itself.
Continuing with the Northwind example from the previous sections, Example 22 shows a service operation that lists Customer entities for a given city.
C#:
public class Northwind : DataService<NorthwindEntities>
{
[WebGet]
public IQueryable<Customer> CustomersByCity(string city)
{
if (string.IsNullOrEmpty(city))
{
throw new ArgumentNullException("city",
"You must provide a city name argument");
}
return this.CurrentDataSource.Customers.Where("it.City = @city",
new ObjectParameter("city", city));
}
}
Public Class Northwind
Inherits DataService(Of NorthwindEntities)
Public Shared Sub InitializeService(...
<WebGet()> _
Public Function CustomersByCity(ByVal city As String) As _
IQueryable(Of Customers)
If String.IsNullOrEmpty(city) Then
Throw New ArgumentNullException("city", _
"You must provide a city name argument")
End If
Dim result = From Customer In Me.CurrentDataSource.Customers _
Where Customer.City = city
Return result.AsQueryable()
End Function
End Class
Example 22: A data service operation to retrieve filtered customers
To invoke the service operation, the usual URL format is used, with query string parameters mapped directly to the method arguments. For example, http://host/northwind.svc/CustomersByCity?city=London . Note that the reference to this.CurrentDataSource gives access to the instance of the NorthwindEntities class used by the data service.
The most interesting aspect of the data service operation example above is that it does not return the final data that needs to be returned to the caller. Instead, it returns a “query” object; specifically, it returns an instance of IQueryable<T>, a class which is part of the Language Integrated Query (LINQ) features added in the .NET Framework v3.5 that represents a query. By returning a query instead of the final data, ADO.NET Data Services can still provide the usual URL options, such as sorting and paging with service operations. For example, /northwind.svc/CustomersByCity?city=London&$orderby=CompanyName would take the resulting query, compose it with the sort option, and return the results of that. If you wish to constrain the service operation such that additional operators cannot be composed, you may return the results of the operation instead of a query object. In this case, the return type of the method should be IEnumerable<T> (or void if the method does not return results). In addition to IQueryable<T>, IEnumerable<T> and void one can return a primitive type or a type representing an entity, directly from the service operation.
The requirements for service operation methods are as follows:
- Must be a public instance method in the data service class
- Must have the [WebGet] attribute to be invoked using HTTP GET requests
- Must have the [WebInvoke] attribute to be invoked using HTTP POST,PUT or DELETE requests
- May return void, IEnumerable<T> (where T represents an Entity Type in the service), T, a primitive value or an IQueryable<T>.In order to support further controlling the results (sorting, paging, etc.), these methods should return IQueryable<T>.
Service-wide visibility of service operations is controlled via a method on the IDataServiceConfiguration class in much the same way Entity Set visibility is controlled. For example, to make the CustomersByCity method in the example above callable, the code in Example 23 must be added to the class derived from DataService.
C#:
public class Northwind : DataService<NorthwindEntities>
{
public static void InitializeService(IDataServiceConfiguration
config)
{
config.SetServiceOperationAccessRule("CustomersByCity",
ServiceOperationRights.All);
}
[WebGet]
public IQueryable<Customer> CustomersByCity(string city)
{… }
}
VB:
Public Class Northwind
Inherits DataService(Of NorthwindEntities)
Public Shared Sub InitializeService(ByVal config As _
IDataServiceConfiguration)
config.SetServiceOperationAccessRule("CustomersByCity", _
ServiceOperationRights.All)
End Sub
<WebGet()> _
Public Function CustomersByCity(ByVal city As String)...
End Class
Example 23: Setting Visibility of Service Operations
Consuming ADO.NET Data Services
AJAX Applications
An ASP.NET AJAX library, available on codeplex, abstracts the details of HTTP away from the application developer, enabling one to work at the JavaScript object level as opposed to manually parsing and creating HTTP requests and responses. A number of “how to” web pages are available on the codeplex site (https://www.codeplex.com/aspnet/Wiki/View.aspx?title=AJAX), provideing code snippets detailing how to use the data service library for AJAX applications.
Using the .NET client library
ADO.NET Data Services includes a minimum-footprint client library that presents a more natural programming model for applications written using the .NET Framework, targeting data services. The client library can return results in terms of .NET objects and handle aspects such as association traversal.
Under the covers, the client library uses HTTP and the AtomPub format, so it will work naturally in corporate networks and internet environments; all that is needed is HTTP-level connectivity to the data service, direct or indirect (e.g. through proxies).
The basics
In order to use the client library, you will need to add a reference to the “System.Data.Services.Client.dll” assembly to your project. The client library can be used from any project type, such as Windows Forms, Windows Presentation Foundation and Web Site projects.
The two main constructs in the client library are the DataServiceContext class and the DataServiceQuery class.
DataServiceContext represents the runtime context of a given data service. While data services themselves are stateless, the context is not, so 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 particular query against the store, specified using the ADO.NET Data Services URL syntax. To execute a query and obtain the results in the form of .NET objects, you simply enumerate over the query object, for example, using the “foreach” construct in C# or “For Each” in Visual Basic.
In order to represent each of the entities defined in the data service as .NET objects in the client, corresponding classes need to be defined for the client application to use. A simple option is to define them manually. Example 24 shows a hand-written definition for the Region class (based on the Northwind’s Region entity), and a small piece of code that executes a query against regions and prints their ID’s and descriptions in the output.
C#:
using System;
using System.Data.Services.Client;
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:1234/Northwind.svc");
IEnumerable<Region> regions = ctx.Execute<Region>(
new Uri("Region?$orderby=RegionID", UriKind.Relative));
foreach (Region r in regions)
{
Console.WriteLine(r.RegionID + ", " + r.RegionDescription);
}
}
}
}
Imports System.Data.Services.Client
Public Class Region
Private _regionID As Integer
Public Property RegionID() As Integer
Get
Return _regionID
End Get
Set(ByVal value As Integer)
_regionID = value
End Set
End Property
Private _regionDescription As String
Public Property RegionDescription() As String
Get
Return _regionDescription
End Get
Set(ByVal value As String)
_regionDescription = value
End Set
End Property
End Class
Module Module1
Sub Main()
Dim ctx As New DataServiceContext(New _
Uri("https://localhost:1234/Northwind.svc"))
Dim regions = ctx.Execute(Of Region)(New _
Uri("Region?$orderby=RegionID", UriKind.Relative))
For Each r In regions
Console.WriteLine(String.Format("{0}, {1}", r.RegionID, _
r.RegionDescription))
Next
Console.ReadLine()
End Sub
End Module
Example 24: Access an Astoria data service from a .NET application using the client library
Automatically generating client side types
While the approach of manually writing the classes works well for a small number of types, as the data service schema grows more complex, the number and size of the classes to manually maintain also grow. To make generating client side types for a target data service simple, ADO.NET Data Services integrates with the “Add Service Reference” wizard in Visual Studio 2008 SP1 to automatically generate client classes for a target data service. Alternatively, a small command line tool ships with the .NET Framework 3.5 SP1 that generates the client side classes just as the “Add Service Reference” feature in Visual Studio does.
Using Add Service Reference to generate client side classes
To generate client classes for interacting with a remote data service using Visual Studio 2008 SP1, follow the below steps:
- Right click the project in the Solution Explorer and select “Add Service Reference” or from the main menu select Project Add Service Reference...
- In the dialog that appears enter the URI of the target data service or if the data service is in the same VS solution click ‘Discover’.
Figure 2: Add Service Reference dialog for a project containing two ADO.NET Data Services
- Select the service to create classes for, specify the namespace to use for the classes and then click the ‘OK’ button
The output from the process is a C# or VB file (depending on project type) that contains a class for each of the entity types in the data service and one class (derived from DataServiceContext) which represents the service as a whole.
The resulting generated classes have members representing primitive values and associations. This facilitates navigating through a graph of associated entities using the object model directly.
Using the command line tool to generate client side classes
The command line tool ships as part of the .NET Framework 3.5 SP1. The tool is called “datasvcutil.exe” and it is located in the directory \Windows\Microsoft.Net\Framework\V3.5. Datasvcutil takes as an argument the base URL to a data service for which to generate types. For example, if the Northwind service is running in the Visual Studio development server at https://localhost:1234/Northwind.svc
, the command –line to generate classes for it would be:
"\Windows\Microsoft.Net\Framework\V3.5\datasvcutil.exe" /out:northwind.cs /uri:“https://localhost:1234/Northwind.svc”
The output from the command is a C# file (Visual Basic types can be generated using the /language:VB switch) that contains a class for each of the entity types in the data service and one class (derived from DataServiceContext) which represents the service as a whole.
The resulting generated classes have members representing primitive values and associations. This facilitates navigating through a graph of associated entities using the object model directly.
Language Integrated Query (LINQ to ADO.NET Data Services)
In addition to querying a data service by specifying a URI path in a call to DataServiceQuery.CreateQuery(…) as shown above, the library supports formulating data service queries using LINQ. The client library handles the details of mapping the LINQ statement to a URI in the target data service and retrieving the specified resources as .NET objects. Example 25 below shows how to retrieve all the customers in the city of London with the result set returned ordered by company name.
C#:
using System;
using System.Data.Services.Client;
using System.Linq;
using NorthwindModel;
namespace TestApplication
{
class Program
{
static void Main(string[] args)
{
// NorthwindEntities is generated by datasvcutil or the “Add
// Service Reference” dialog and derives from DataServiceContext
NorthwindEntities ctx = new
NorthwindEntities("https://localhost:1234/Northwind.svc");
ctx.MergeOption = MergeOption.AppendOnly;
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);
}
}
}
}
VB:
Imports System.Data.Services.Client
Imports SimpleDataServiceClient.NorthwindModel
Module Module1
Sub Main()
'NorthwindEntities is generated by datasvcutil or the “Add
' Service Reference” dialog and derives from DataServiceContext
Dim ctx As New NorthwindEntities(New _
Uri("https://localhost:1234/Northwind.svc"))
ctx.MergeOption = MergeOption.AppendOnly
Dim q = From c In ctx.Customers _
Where c.City = "London" _
Order By c.CompanyName _
Select c
For Each cust In q
Console.WriteLine(cust.CompanyName)
Next
Console.ReadLine()
End Sub
End Module
Example 25: Retrieving all customers in the city of London, ordered by company name
NOTE:* *The set of queries expressible in the LINQ syntax is broader than those enabled in the REST-based URL syntax used by data services. An exception will be thrown if the query cannot be mapped to a URL in the target data service. As a general rule for this constraint, think hierarchical traversal. Any query which needs two or more pivots (joins, subqueries such as using "Any", etc.) cannot currently be mapped to a URL. Queries with a single pivot and association traversals will generally work (access a "many" property, access many and then restrict to one, and then access many again, etc) Filters for non-key criteria, as well as sorting/paging, only work on the thing you're retuning, never the intermediate entities.
Associations
Associations between objects are tracked and managed by the DataServiceContext class. You can load associated objects eagerly or as needed, by using the URL formats discussed in the previous sections outlining the addressing scheme for ADO.NET Data Service URLs. When loading associated entities as needed, the LoadProperty method on the DataServiceContext class is used. Example 26 shows how to delay-load Product entities associated with a particular Category.
C#:
using System;
using System.Data.Services.Client;
using NorthwindModel;
namespace TestApplication
{
class Program
{
static void Main(string[] args)
{
DataServiceContext ctx = new
DataServiceContext(new
Uri("https://localhost:1234/Northwind.svc"));
ctx.MergeOption = MergeOption.AppendOnly;
// get a single category
IEnumerable<Categories> categories = ctx.Execute<Categories>(
new Uri("Categories(1)", UriKind.Relative));
foreach (Categories c in categories)
{
Console.WriteLine(c.CategoryName);
ctx.LoadProperty(c, "Products");
foreach (Products p in c.Products)
{
Console.WriteLine("\t" + p.ProductName);
}
}
}
}
}
Imports System.Data.Services.Client
Imports SimpleDataServiceClient.NorthwindModel
Module Module1
Sub Main()
Dim ctx As New DataServiceContext(New _
Uri("https://localhost:1234/Northwind.svc"))
ctx.MergeOption = MergeOption.AppendOnly
'Get a single category
Dim categories = ctx.Execute(Of Categories)(New _
Uri("Categories(1)", UriKind.Relative))
For Each c In categories
Console.WriteLine(c.CategoryName)
ctx.LoadProperty(c, "Products")
For Each p In c.Products
Console.WriteLine(vbTab & p.ProductName)
Next
Next
Console.ReadLine()
End Sub
End Module
Example 26: Delay-loading related entities using the Load() method
In certain scenarios, you know that you will need the associated objects, and you may want to avoid the added latency of an extra request to fetch them. In this case you can use the “expand” option within the URL. The client library will recognize that the result includes both top-level entities and associated entities and will materialize all of them as an object graph. Example 27 is similar to the previous example, but eagerly loads the related products in a single round-trip to the data service.
C#:
using System;
using System.Data.Services.Client;
using NorthwindModel;
namespace TestApplication
{
class Program
{
static void Main(string[] args)
{
DataServiceContext ctx = new
DataServiceContext(new
Uri("https://localhost:1234/Northwind.svc"));
ctx.MergeOption = MergeOption.AppendOnly;
// get a single category
IEnumerable<Categories> categories = ctx.Execute<Categories>( new
Uri("Categories(1)?$expand=Products", UriKind.Relative));
foreach (Categories c in categories)
{
Console.WriteLine(c.CategoryName);
foreach (Products p in c.Products)
{
Console.WriteLine("\t" + p.ProductName);
}
}
}
}
}
VB:
Imports System.Data.Services.Client
Imports SimpleDataServiceClient.NorthwindModel
Module Module1
Sub Main()
Dim ctx As New DataServiceContext(New _
Uri("https://localhost:36925/Northwind.svc"))
ctx.MergeOption = MergeOption.AppendOnly
'Get a single category
Dim categories = ctx.Execute(Of Categories) _
(New Uri("Categories(1)?$expand=Products", UriKind.Relative))
For Each c In categories
Console.WriteLine(c.CategoryName)
For Each p In c.Products
Console.WriteLine(vbTab & p.ProductName)
Next
Next
Console.ReadLine()
End Sub
End Module
Example 27: Using "expand" to eagerly-load related entities
Update support
To create a new instance in the data service, create the .NET object and then call AddObject() in the DataServiceContext instance being used, passing the object and the target entity-set as shown in Example 28.
C#:
using System;
using System.Data.Services.Client;
using NorthwindModel;
namespace TestApplication
{
class Program
{
static void Main(string[] args)
{
DataServiceContext ctx = new
DataServiceContext(new
Uri("https://localhost:1234/Northwind.svc"));
ctx.MergeOption = MergeOption.AppendOnly;
Categories c = new Categories();
c.CategoryName = "NewCategory1";
ctx.AddObject("Categories", c);
ctx.SaveChanges();
}
}
}
VB:
Imports System.Data.Services.Client
Imports SimpleDataServiceClient.NorthwindModel
Module Module1
Sub Main()
Dim ctx As New DataServiceContext(New _
Uri("https://localhost:1234/Northwind.svc"))
ctx.MergeOption = MergeOption.AppendOnly
Dim c As New Categories()
c.CategoryName = "NewCategory1"
ctx.AddObject("Categories", c)
ctx.SaveChanges()
End Sub
End Module
Example 28: Inserting a new entity instance using the client library
After an entity is created or modified in the data service, the service returns a fresh copy of the entity including any values that may have been updated as a result of triggers in the database, auto-generated keys, etc. The client library will automatically update the .NET object with these new values.
To modify an existing entity instance, first query for that object, then make the desired changes to its properties and finally call the UpdateObject method to indicate to the client library that it needs to send an update for that object as seen in Example 29.
NOTE:* *The example below uses a context class (NorthwindEntities) that was created by the code generation tool above and derives from DataServiceContext. This derived class provides service specific properties to simplify coding against a given service.
C#:
using System;
using System.Data.Services.Client;
using System.Linq;
using NorthwindModel;
namespace TestApplication
{
class Program
{
static void Main(string[] args)
{
NorthwindEntities ctx = new
NorthwindEntities("https://localhost:1234/Northwind.svc");
ctx.MergeOption = MergeOption.AppendOnly;
var c1 = (from c in ctx.Categories
where c.CategoryName == "NewCategory1"
select c).First();
c1.CategoryName = "UpdatedCategory";
ctx.UpdateObject(c1);
ctx.SaveChanges();
}
}
}
VB:
Imports System.Data.Services.Client
Imports SimpleDataServiceClient.NorthwindModel
Module Module1
Sub Main()
Dim ctx As New NorthwindEntities(New _
Uri("https://localhost:1234/Northwind.svc"))
ctx.MergeOption = MergeOption.AppendOnly
Dim c1 = (From c In ctx.Categories _
Where c.CategoryName = "NewCategory1").First()
c1.CategoryName = "UpdatedCategory"
ctx.UpdateObject(c1)
ctx.SaveChanges()
End Sub
End Module
Example 29: Updating an existing entity using the client library
To delete an entity instance, call the Delete method in DataServiceContext.
Changes are tracked in the DataServiceContext instance but not sent to the server immediately. Once you are done with the required changes for a given activity, call SaveChanges() to submit all the changes to the data service.
The update infrastructure can also deal with association changes. It is possible to change associations between .NET objects and have the client library reflect those changes as association creation/deletion operations in the HTTP interface. To illustrate this, Example 30 creates a Product in the Northwind database and associates it with an existing Category. Categories and products participate in a one-to-many association; therefore, a given product has one specific category.
C#:
using System;
using System.Data.Services.Client;
using System.Linq;
using NorthwindModel;
namespace TestApplication
{
class Program
{
static void Main(string[] args)
{
NorthwindEntities ctx = new
NorthwindEntities("https://localhost:1234/Northwind.svc");
ctx.MergeOption = MergeOption.AppendOnly;
var c1 = (from c in ctx.Categories
where c.CategoryName == "UpdatedCategory"
select c).First();
Products p = new Products();
p.ProductName = "TestProduct";
p.Discontinued = false;
p.QuantityPerUnit = "1";
p.ReorderLevel = 100;
p.UnitPrice = 1.1M;
p.UnitsInStock = 200;
p.UnitsOnOrder = 0;
ctx.AddToProducts(p);
// Set link between product and category
p.Categories = c1;
ctx.SetLink(p, "Categories", c1);
ctx.SaveChanges();
Console.ReadKey();
}
}
}
Imports System.Data.Services.Client
Imports SimpleDataServiceClient.NorthwindModel
Module Module1
Sub Main()
Dim ctx As New NorthwindEntities(New _
Uri("https://localhost:1234/Northwind.svc"))
ctx.MergeOption = MergeOption.AppendOnly
Dim c1 = (From c In ctx.Categories _
Where c.CategoryName = "UpdatedCategory").First()
Dim p As New Products()
p.ProductName = "TestProduct"
p.Discontinued = False
p.QuantityPerUnit = "1"
p.ReorderLevel = 100
p.UnitPrice = 1.1D
p.UnitsInStock = 200
p.UnitsOnOrder = 0
ctx.AddToProducts(p)
'Set link between product and category
p.Categories = c1
ctx.SetLink(p, "Categories", c1)
ctx.SaveChanges()
End Sub
End Module
Example 30: Creating a product entity and associate it with a Category
NOTE:* *Managing modifications to arbitrary object graphs with bidirectional associations is a complex problem; there are advanced libraries such as the ADO.NET Entity Framework that offer very rich, highly consistent state managers for handling partially materialized graphs. The ADO.NET Data Service client library, on the other hand, is designed for minimum footprint and provides the primitives required to enable mapping data service operations to .NET objects. For a future release, we are evaluating layering higher level semantics on top of the primitives demonstrated above.
Batching
So far, each of the examples have resulted in one operation on the client mapping to one HTTP request. While this approach, single [deep] operation per HTTP request, works well, it can be beneficial for a client to “batch” up a group of operations and send them to the data service in a single HTTP request. This reduces the number of roundtrips to the data service and enables a logical scope of atomicity for sets of operations. To support such requirements, the client supports sending groups of CUD (Insert,Update,Delete) operations and groups of Query operations to a data service in a single HTTP request.
Example 31 shows how to send a group of two query operations to a data service in a single HTTP request.
C#:
using System;
using System.Data.Services.Client;
using System.Linq;
using NorthwindModel;
namespace TestApplication
{
class Program
{
static void Main(string[] args)
{
NorthwindEntities ctx = new
NorthwindEntities("https://localhost:1234/Northwind.svc");
ctx.MergeOption = MergeOption.AppendOnly;
var q = (DataServiceRequest)from o in ctx.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:1234/Northwind.svc/Customers")),
q);
// enumerate over the response object to process the results
}
}
}
VB:
Imports System.Data.Services.Client
Imports SimpleDataServiceClient.NorthwindModel
Module Module1
Sub Main()
Dim ctx As New NorthwindEntities(New _
Uri("https://localhost:1234/Northwind.svc"))
ctx.MergeOption = MergeOption.AppendOnly
Dim q1 = CType((From o In ctx.Orders), DataServiceRequest)
Dim q2 = New DataServiceRequest(Of Customers)( _
New Uri("https://localhost:1234/Northwind.svc/Customers"))
'send two queries in one batch request to the data service
Dim response = ctx.ExecuteBatch(q2, q1)
'enumerate over the response object to process the results
For Each q As QueryOperationResponse In response
If q.Query.ElementType Is GetType(Customers) Then
For Each c As Customers In q
Console.WriteLine(c.CompanyName)
Next
End If
If q.Query.ElementType Is GetType(Orders) Then
For Each o As Orders In q
Console.WriteLine(o.OrderID)
Next
End If
Next
Console.ReadLine()
End Sub
End Module
Example 31: Sending queries as a batch request
To send a group of CUD operations as a batch to the data service, simply call the SaveChanges() method passing SaveChangesOptions.Batch as the only parameter. This parameter value instructs the client to batch all the pending change operations into a single batch and send them to the data service. For example, changing the line ctx.SaveChanges();
to ctx.SaveChanges(SaveChangesOptions.Batch);
would cause the previous operations to be sent to the data service in a single batch (using one HTTP request). When change operations are sent as a batch using SaveChangesOptions.Batch, the semantics are such that either all the changes will complete successfully or none of the changes will be applied. Said another way, calling SaveChanges with SaveChangesOptions.Batch as the parameter informs the data service to process the changes as an atomic group of operations (all operations succeed or none do).
Authentication in the client library
The .NET client library is built on top of the .NET framework support for the HTTP protocol. Among other things, the framework infrastructure handles various authentication schemes over HTTP automatically, given a set of credentials.
By default, the client library will not supply any credentials to the HTTP stack. However, you can set the Credentials property in DataServiceContext to point to an object that implements the ICredentials interface. For more information on credentials, see WebRequest.Credentials in the MSDN documentation at https://msdn.microsoft.com.
If the remote data service does not use transport based authentication (HTTP Basic, digest, etc), then the DataServiceContext.SendingRequest event can be used to set custom authentication information (HTTP headers, etc) on any HTTP request issued by the client library.
Asynchronous interactions with the data service
Web applications must be designed to consider the fact that interactions with the server tend to have higher latency than typical applications that run inside internal networks. The use of Asynchronous interactions with the server helps maintain a responsive user interface, while the application is waiting for a response from the server.
The ADO.NET Data Services client library supports an asynchronous mode of operation for many of the operations available on the DataServiceContext class such as retrieval and saving changes.
C#:
using System;
using System.Data.Services.Client;
using System.Linq;
using NorthwindModel;
namespace TestApplication
{
class Program
{
static void Main(string[] args)
{
NorthwindEntities ctx = new
NorthwindEntities("https://localhost:51905/nw.svc");
ctx.MergeOption = MergeOption.AppendOnly;
DataServiceQuery<Customers> q = ctx.Customers;
q.BeginExecute(
delegate(IAsyncResult ar)
{
foreach (Customers c in q.EndExecute(ar))
{
Console.WriteLine(c.CompanyName);
}
},
null);
}
}
}
Imports System.Data.Services.Client
Imports SimpleDataServiceClient.NorthwindModel
Module Module1
Private MyCustomers As DataServiceQuery(Of Customers)
Sub Main()
Dim ctx As New NorthwindEntities(New _
Uri("https://localhost:1234/Northwind.svc"))
ctx.MergeOption = MergeOption.AppendOnly
MyCustomers = ctx.Customers
MyCustomers.BeginExecute(AddressOf MyCallback, Nothing)
Console.ReadLine()
End Sub
Private Sub MyCallback(ByVal ar As IAsyncResult)
For Each c In MyCustomers.EndExecute(ar)
Console.WriteLine(c.CompanyName)
Next
End Sub
End Module
Example 32: Using the asynchronous API in the client library
Using the client library from Microsoft Silverlight 2
A Silverlight-based client library for ADO.NET Data Services is available as part of the Silverlight 2 SDK. The Silverlight 2 library has a very similar same programming experience to the client library provided for the .NET Framework. The primary differences include that the SendingRequest event and the Credentials property found in the .NET Client are not available in Silverlight and that the library is limited to making same domain requests. At present, the Silverlight library uses the browsers HTTP stack for communication and thus takes advantage of browser-based UI dialogs for credential collection.
Controlling Data Service Policies
Authentication
ADO.NET Data services do not introduce a new authentication scheme. Rather, Data Services rely on the existing ASP.NET/WCF authentication infrastructure. If you have an ASP.NET site that uses authentication (either one of the built-in authentication schemes or a custom one that sets the HTTP context principal appropriately), ADO.NET Data Services leverage that mechanism to establish the current identity (principal) for a given request. Similarly, if your data service is hosted outside ASP.NET (WCF or via a custom host) the host may choose to use any authentication mechanism so long as it provides APIs for a data service author to access the principle of the request. For one example of how to integrate the ASP.NET infrastructure into your data services solutions see: https://blogs.msdn.com/astoriateam/archive/2008/05/27/securing-data-services.aspx.
Authorization & Validation
By default, a data service is completely locked down with no read or write access. One of the first steps a data service developer will need to take is to open up access to resources exposed by the data service. ADO.NET Data Services supports a number of mechanisms to control authorization policy and perform per request validation as defined in the following sections.
Setting service wide read/write access policy can be done on a per service basis using a service initialization method as shown in Example 33 below:
C#:
public class MyService : DataService<Northwind>
{
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.WriteAppend |
EntitySetRights.WriteMerge |
EntitySetRights.WriteReplace);
}
}
VB:
Imports System.Data.Services
Imports System.Linq
Imports System.ServiceModel.Web
Public Class Northwind
Inherits DataService(Of NorthwindEntities)
Public Shared Sub InitializeService(ByVal config As _
IDataServiceConfiguration)
'URI "/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.WriteAppend Or EntitySetRights.WriteReplace)
End Sub
End Class
Example 33: Setting service-wide access policy
The table below lists all the rights that can be granted per entity set. As shown in the example above, rights can be combined together to build composite policies per Entity Set.
Resource Container Rights | Description |
---|---|
None |
The set has no read/write access and does not appear in the services metadata document |
ReadSingle |
Queries by key are allowed on the set |
ReadMultiple |
Queries for the entire contents of the set are allowed (does not imply query by key is allowed) |
WriteAppend |
New resources can be inserted into the set |
WriteMerge |
Update existing resources by merging changes into the existing entity |
WriteReplace |
Update existing resources by replacing the entity |
WriteDelete |
Existing resources within the set can be deleted |
AllRead |
Shorthand for ReadSingle | ReadMultiple |
AllWrite |
Shorthand for WriteInsert | WriteUpdate | WriteDelete |
All |
Full read/write access to the set |
Table 9: Entity Rights available per entity set.
In addition to limiting access to entity sets, the rights associated with an entity set also restrict what is available from the metadata endpoint for the service. If an Entity Set has access rights equal to ‘None’ (the default setting) then any HTTP request to an entity in the Entity Set results in a 404 (Not Found) response and the Entity Set will not appear in the metadata document (ie. CSDL document) describing the data service.
_NOTE:* *In addition to removing the Entity Set from metadata all EDM association artifacts (associations, etc) related to the Entity Set are also removed. If an Entity Set has access rights greater than ‘None’, then the set and associated artifacts (associations, etc) are visible in the response from the data service’s metadata document. _
Intercepting Entities on Retrieval and Update
Many data services will need to run validation logic when entities enter the data service (for inserts, updates or deletes) and/or restrict access to entities on a per request basis. For these scenarios, Interceptors are used which enable 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, assume you want to implement a policy which enabled customers to only retrieve their orders and not orders placed by other customers. Example 34 shows a query interceptor that implements this access policy. The important aspect to note about query interceptors is that they return a predicate which is then appended (by the ADO.NET Data Services Framework) to the query that the system will push down to the underlying data store to retrieve the requested entity. As shown in the example, this approach enables access policy to be defined using query composition eliminating any added round trips to the data store to retrieve access control information. This example assumes the data service is hosted within a WCF+ASP.NET web application with authentication enabled. The ASP.Net HTTPContext object is used to retrieve the principle of the current request.
C#:
using System;
using System.Web;
using System.Collections.Generic;
using System.ServiceModel.Web;
using System.Linq;
using System.Data.Services;
using NorthwindModel;
namespace TestApplication
{
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.Customers.ContactName ==
HttpContext.Current.User.Identity.Name
}
}
}
VB:
Imports System.Data.Services
Imports System.Linq
Imports System.ServiceModel.Web
Imports System.Linq.Expressions
Public Class Northwind
Inherits DataService(Of NorthwindEntities)
Public Shared Sub InitializeService(ByVal config As _
IDataServiceConfiguration)
config.SetEntitySetAccessRule("Orders", EntitySetRights.All)
End Sub
<QueryInterceptor("Orders")> _
Public Function OnQueryOrders() As Expression(Of Func(Of Orders, Boolean))
Return Function(o) o.Customers.ContactName = _
HttpContext.Current.User.Identity.Name
End Function
End Class
Example 34: Query interceptor method implementing a custom, per request access policy
In addition to intercepting queries, update operations (insert, update & delete) also support interceptors. For example, you may want to validate all the Category entities that are created or updated to make sure they consist of a single English word. Example 35 shows an interceptor that does this validation (without using a dictionary for simplicity).
C#:
using System;
using System.Web;
using System.Collections.Generic;
using System.ServiceModel.Web;
using System.Linq;
using System.Data.Services;
using NorthwindModel;
namespace TestApplication
{
public class nw : DataService<NorthwindModel.NorthwindEntities>
{
public static void InitializeService(IDataServiceConfiguration
config)
{
config.SetEntitySetAccessRule("Categories",
EntitySetRights.All);
}
[ChangeInterceptor("Category")]
public void OnChangeCategories(Category c, UpdateOperations ops)
{
if (ops == UpdateOperations.Add ||
ops == UpdateOperations.Change)
{
// single word, no spaces
if (c.Name.Contains(" "))
{
throw new DataServiceException(400,
"Category names must consist of a single word");
}
// must be an English word
// if(!Dictionary.Lookup(category.CategoryName)) throw new
// DataServiceException(...)
// if no error found, simply return and the Category will be
// inserted/updated
}
}
}
VB:
Imports System.Data.Services
Imports System.Linq
Imports System.ServiceModel.Web
Public Class Northwind
Inherits DataService(Of NorthwindEntities)
Public Shared Sub InitializeService(ByVal config As _
IDataServiceConfiguration)
config.SetEntitySetAccessRule("Categories", EntitySetRights.All)
End Sub
<ChangeInterceptor("Categories")> _
Public Sub OnChangeCategories(ByVal c As Categories, ByVal ops As _
UpdateOperations)
If ops = UpdateOperations.Add OrElse ops = UpdateOperations.Change Then
'Single word, no spaces
If c.CategoryName.Contains(" ") Then
Throw New DataServiceException(400, _
"Category names must consist of a single word")
End If
' Must be an English word:
'If (!Dictionary.Lookup(c.CategoryName)) Then
' throw new DataServiceException(...)
'End If
' If no error was found, simply return and the Category
' will be inserted/updated
End If
End Sub
End Class
Example 35: Update interceptor method that validates input Category entities before being persisted in the underlying store
For update interceptors (methods bound to creation, update or deletion of an entity), if the interceptor method throws an exception then the operation is aborted, no change is made in the database, and an error is returned to the client agent.
To create a query interceptor that is notified when querying entities of a given entity-set:
- Create a public method in your data service class, annotated with the [QueryInterceptor(“<EntitySetName>”)] attribute, where <EntitySetName> is the name of the set the interceptor applies to
- The method must return Expression<Func<T,bool>> and accept no parameters, where T is the name of the base type for the entity set specified in the QueryInterceptor attribute
- If the method throws an exception, request processing is complete and the error is returned in the HTTP response
To create a change interceptor that is notified when entities of a given entity-set are created, modified or deleted:
- Create a public method in your data service class, annotated with the [ChangeInterceptor(“<EntitySetName>”)] attribute, where <EntitySetName> is the name of the set the interceptor applies to
- Define the method such that it returns void and takes two arguments, an object of the type contained in the entity set identified in the ChangeInterceptor attribute and a UpdateOperations enumeration that defines the action (Add, Change or Delete) being requested on the resource.
- The interceptor may alter the object passed in or set the object reference to an entirely different instance
- If the method throws an exception, request processing is complete and an error status code is returned in the HTTP response without effecting the underlying data store
HTTP Caching
One of the key goals of ADO.NET Data Services is that they are designed to naturally integrate with the rest of the web infrastructure. HTTP caching is a good example of this. One easy way of gaining performance in data services is by enabling caching at the HTTP level. This is useful when it is acceptable for client agents to consume data that may be stale coming from a cache. The current release of ADO.NET Data Services does not provide APIs to control HTTP caching semantics of responses; however, a service developer could control such semantics via a custom host, interceptors or any mechanism which allows direct access to the headers of the HTTP response object.
Error Handling
A number of issues could cause a request to a data service to be aborted or fail. For example, assume a data service exists with an update interceptor which includes validation logic. Further assume the validation logic failed because the client was trying to insert a new Employee entity and the value of the ‘age’ property was not at least 16. A typical application would want to update the UI that accepted the Employee input and perhaps put a red ‘*’ beside the text box with the offending age value. In “traditional” web development this is simple as the UI generation and business logic exist at the same tier, but for a client of data services (ex. AJAX client calling a data service) the error must be serialized and sent to the client such that it can update its UI.
In order for a data service to transfer a structured error to the client, all errors raised on the service must be in the form of an exception of the type DataServiceException. This exception type is special in that the service knows how to serialize the public properties of this type to a fixed error payload, thus enabling a service author to control the error information sent to a client.
Since it is not practical to assume all components used in a data service’s implementation throw DataServiceException exceptions, a method is defined on the server side which gets invoked whenever an unhandled exception occurs during the processing of a data service request. In this method, the data service author should map the exception thrown to an instance of the DataServiceException type. This is shown below in Example 36.
Request:
POST /Customers HTTP/1.1
Accept: application/atom+xml, application/xml
Content-Type: application/atom+xml
[representation of a new customer with an invalid CompanyName]
Service Code in C#:
public class contacts : DataService<NorthwindModel.NorthwindEntities>
{
public static void InitializeService(
IDataServiceConfiguration config)
{
config.SetEntitySetAccessRule("*", EntitySetRights.All);
config.UseVerboseErrors = false;
}
protected override void HandleException(HandleExceptionArgs args)
{
if(args.Exception.InnerException is ArgumentException){
ArgumentException e = (ArgumentException)
args.Exception.InnerException;
args.Exception = new DataServiceException(400,
"PropertySyntaxError:" + e.ParamName,
"human readable description of problem",
"en-US",
e);
}
}
}
Service Code in VB:
Public Class Northwind
Inherits DataService(Of NorthwindEntities)
Public Shared Sub InitializeService(ByVal config As _
IDataServiceConfiguration)
config.SetEntitySetAccessRule("*", EntitySetRights.All)
config.UseVerboseErrors = False
End Sub
Protected Overrides Sub HandleException(ByVal args As HandleExceptionArgs)
If TypeOf args.Exception.InnerException Is ArgumentException Then
Dim e = CType(args.Exception.InnerException, ArgumentException)
args.Exception = New DataServiceException(400, _
"PropertySyntaxError:" & e.ParamName, _
"human readable description of problem", _
"en-US", _
e)
End If
End Sub
End Class
Response:
HTTP/1.1 400 Bad Request
Content-Type:application/xml
…
<error
xmlns=”https://schemas.microsoft.com/ado/2007/08/dataservices”
xmlns:m=”https://schemas.microsoft.com/ado/2007/08/dataservices/metadata”>
<code>PropertySyntaxError:CompanyName</code>
<message xml:lang=”en-US”>human readable description of problem</message>
</error>
Example 36: Assume a validation error occurred while processing a request which caused an ArgumentException to be thrown invoking the exception handler shown in the ‘service code’ section.
Leveraging the ADO.NET Entity Framework mapping engine
The ADO.NET Data Services runtime library is built on top of the ADO.NET Entity Framework, which is the first concrete implementation of the Entity Data Model.
In its initial form, the Entity Framework is designed to expose EDM schemas for data stored in relational databases. In order to present an EDM view of this data, the Entity Framework includes a powerful mapping engine. The Entity Framework allows developers and data designers to design an EDM schema that is appropriate for the applications target environment, and then to declaratively describe the mapping between that EDM schema and the logical database schema that will support the data.
This is interesting from the ADO.NET Data Services perspective because the mapping infrastructure allows you to re-shape the data that is surfaced in the data service, so that it does not follow the same logical schema that is required for the database.
The declarative mapping specification is persisted in XML form, and can be in a stand-alone mapping file (typically with extension .msl) or part of an EDM designer file (.edmx). If you create a new data service following the steps enumerated at the beginning of this document, you will have an edmx file in the project that you can examine. While you can look at the XML directly, you will typically work with these edmx files using the visual EDM designer, which depicts EDM schemas. If you open an edmx file in the visual designer (the default in Visual Studio) you can also see the mappings in addition to the schema itself by right-clicking on the design surface and selecting “Show Entity Mapping Details”.
A few examples of what can be done using the mapping engine include:
- Renaming:
- Many-to-many associations:
- Stored-procedures for change processing:
- Custom mapping views:
Mapping is an extensive topic. In ADO.NET Data Services, mapping relies entirely on the Entity Framework infrastructure. For more information on mapping, see the ADO.NET Entity Framework documentation.
Deployment notes
Visual Studio development server versus IIS
Typically during development you will use the Visual Studio development web server. When deploying your application, however, you will most likely be deploying on a different server, typically Internet Information Server (IIS) in the case of .NET-based web applications.
There are a couple of configuration aspects to consider when deploying a web application with data services in IIS:
- HTTP verb configuration.
- Database authentication credentials.
Making changes to IIS configuration or adjusting the database/web server credentials to work properly in a production environment typically require some background knowledge of the deployment. Consult with your system administrator on the management and security implications of any of these changes.