Microsoft Data
Platform Development Technical Article
Mike Flasko
Microsoft® Corporation
Published: August 2008
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 http://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="http://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="http://schemas.microsoft.com/ado/2007/08/dataservicesdataservices"
xmlns:m="http://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="http://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="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Categories"
title="Categories"
href="Products(1)/Categories"
type="application/atom+xml;type=entry"
/>
<link
rel="http://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="http://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>
>Example 4: 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="http://localhost:63952/northwind.svc/"
xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
xmlns="http://www.w3.org/2005/Atom">
<id>http://localhost: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="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Categories"
type="application/atom+xml;type=entry" title="Categories"
href="Products(1)/Categories" />
<link
rel="http://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="http://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="http://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.
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.
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.
Table 3: String Functions
Table 4: Date Functions
Table 5: Math Functions
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.
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.
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=http://server/service.svc
xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
<title/>
<id>http://server/service.svc/Customer(’ALFKI’)</id>
<updated/>
<category
term="NorthwindModel.Customers"
scheme="http://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="http://server/service.svc/"
xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservicesdataservices"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
xmlns="http://www.w3.org/2005/Atom">
<id>http://server/service.svc/Customers</id>
<updated />
<title>Customers</title>
<link rel="self" href="Customers"
title="Customers" />
<entry>
<id>http://server/service.svc/Customers('ALFKI')</id>
<updated />
<title />
<author>
<name />
</author>
<category term="NorthwindModel.Customers"
scheme=http://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="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Orders"
title="Orders"
href="Customers('ALFKI')/Orders"
type="application/atom+xml;type=feed" />
<link
rel="http://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=”http://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="http://localhost:63952/northwind.svc/"
xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m=http://schemas.microsoft.com/ado/2007/08/dataservices/metadata
xmlns="http://www.w3.org/2005/Atom">
<id>http://localhost: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="http://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>http://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>http://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="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Customers"
type="application/atom+xml;type=entry"
title="Customers"
href="Orders(10643)/Customers" />
<link
rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Employees"
type="application/atom+xml;type=entry" title="Employees"
href="Orders(10643)/Employees" />
<link
rel="http://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="http://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="http://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="http://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. http://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="http://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>
>Example 9: 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.
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:
"http://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:
"http://server/service.svc/Customers(\'ALFKI\')/Orders"
}
},
CustomerDemographics: {
__deferred: {
uri:
“http://server/service.svc/Customers(\'ALFKI\')/CustomerDemographics"
}
}
}
}
>>Example 10: 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:
"http://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:
"http://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:
"http://server/service.svc/Orders(10643)/Customers"
}
},
Employees:
{
__deferred: {
uri:
"http://server/service.svc/Orders(10643)/Employees"
}
},
Order_Details: {
__deferred: {
uri:
"http://server/service.svc/Orders(10643)/Order_Details"
}
},
Shippers: {
__deferred: {
uri:
"http://localhost:51905/nw.svc/Orders(10643)/Shippers"
}
}
},
// Additional
order objects
]
CustomerDemographics: {
__deferred: {
uri:
“http://server/service.svc/Customers(\'ALFKI\')/CustomerDemographics"
}
}
}
}
>Example 11: 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="http://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
xmlns="http://www.w3.org/2005/Atom">
<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="http://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
xmlns="http://www.w3.org/2005/Atom">
<id>http://host/northwind.svc/Categories(11)</id>
<updated />
<title />
<author>
<name
/>
</author>
<link rel="edit"
href="Categories(11)" title="Categories" />
<category
term="NorthwindModel.Categories"
scheme="http://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="
http://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="http://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
xmlns="http://www.w3.org/2005/Atom">
<content
type="application/xml">
<m:properties>
<d:CategoryName>Changed Category</d:CategoryName>
</m:properties>
</content>
</entry>
JSON:
{
CategoryName:
"Changed Category"
}
>Example 14: 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="http://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
xmlns="http://www.w3.org/2005/Atom">
<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="http://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
xmlns="http://www.w3.org/2005/Atom"
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="http://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)' }
}
}
>Example 16: 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="http://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="http://schemas.microsoft.com/ado/2007/08/dataservices">
http://service/northwind.svc/Employees(2)</uri>
JSON:
{ "uri": "
http://service/northwind.svc/Employees(2)" }
>Example 18: 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="http://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
xmlns="http://www.w3.org/2005/Atom"
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="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Region"
title="Region" href="Region(4)">
</link>
<link
rel="http://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.
.jpg)
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="http://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m="http://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="http://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="http://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));
}
}
>VB:
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 (http://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("http://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);
}
}
}
}
>VB:
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("http://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’.
.jpg)
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 http://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:“http://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("http://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("http://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("http://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);
}
}
}
}
}
>VB:
Imports System.Data.Services.Client
Imports SimpleDataServiceClient.NorthwindModel
Module Module1
Sub Main()
Dim ctx As New DataServiceContext(New _
Uri("http://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("http://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("http://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("http://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("http://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("http://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("http://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("http://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();
}
}
}
>VB:
Imports System.Data.Services.Client
Imports SimpleDataServiceClient.NorthwindModel
Module Module1
Sub Main()
Dim ctx As New NorthwindEntities(New _
Uri("http://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("http://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("http://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("http://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("http://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 http://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("http://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);
}
}
}
>VB:
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("http://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: http://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.
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=”http://schemas.microsoft.com/ado/2007/08/dataservices”
xmlns:m=”http://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.