Azure + Bing Maps: Expose data to the world with WCF Data Services
This is the sixth article in our "Bring the clouds together: Azure + Bing Maps"
series. You can find a preview of live demonstration on
https://sqlazurebingmap.cloudapp.net/. For a list of articles in the series,
please refer to
Introduction
In our
previous post, we introduced how to access spatial data using
ADO.NET Entity Framework. This works well if you're working on a simple
N-tire solution, and all tires are based on .NET. But very often, a
cloud application must talk to the remaining of the world. For example,
you may want to expose the data to third party developers, and allow
them to use whatever platform/technology they like. This is where web
service comes to play, and this post will focus on how to expose the
data to the world using WCF Data Services.
Before reading, we assume you have a basic understanding of the
following technologies:
DO.NET Entity Framework (EF). If you're new to EF, please refer to
the MSDN
tutorial to get started. You can also find a bunch of getting started
video tutorials on the Silverlight web site, such asthis one. This post assumes you have a EF model ready to use.
WCF Data Services (code name Astoria). If you're new to Astoria, please refer to
the MSDN
tutorial to get started.
The above Silverlight video tutorial covers
Astoria as well. This post targets users who know how to expose a WCF Data
Service using Entity Framework provider with the default configuration, but
may not understand more advanced topics such as reflection provider.A SQL Azure account if you want to use SQL Azure as the data store.
WCF Data Services are WCF
Before you go, please keep in mind that WCF Data Services are WCF
services. So everything you know about WCF applies to WCF Data Services.
You should always think in terms of service, rather than data accessing.
A data service is a service that exposes the data to the clients, not a
data accessing component.
A WCF Data Service is actually a REST service with a custom service host
(which extends WCF's WebServiceHost). You can do everything using a
regular WCF REST service. But the benefit of using data services are:
- It implements the OData protocol for
you, which is widely adopted. Products such as Windows Azure table storage,
SharePoint 2010, Excel PowerPivot, and so on, all use OData. - It provides a provider independent infrastructure. That is, the data
provider can be an Entity Framework model, a CLR object model, or your
custom data provider that may work against, say Windows Azure table storage.
With a regular WCF REST service, you will have to implement everything on
your own.
WCF Data Services data providers
There're 3 kinds of data providers, as listed in
https://msdn.microsoft.com/en-us/library/dd672591.aspx: Entity
Framework provider, reflection provider, and custom provider.
The simplest way to create a WCF Data Service is to use Entity Framework
as the data provider, as you can find in most tutorials. But you must be
aware of the limitations.
When using Entity Framework as the data provider for a WCF Data Service,
the service depends heavily on the EF conceptual model, and thus storage
model as well. That means if you have custom properties in the model,
like our sample's model, those custom properties will not be exposed to
the clients.
This is unfortunately a limitation in the current version of data
service. There's no workaround except for using a reflection provider or
custom provider, instead of EF provider.
Our sample uses reflection provider, because it meets our requirement.
If you need more advanced features, such as defining a custom metadata,
you can use custom providers. Please refer to
https://msdn.microsoft.com/en-us/library/ee960143.aspx for more
information.
Create a read only reflection provider
You can think reflection provider as a plain CLR object model. For a
read only provider, you can take any CLR class, as long as they meet the
following:
- Have one or more properties marked as DataServiceKey.
- Do not have any properties that cannot be serialized by
DataContractSerializer, or put those properties into the IgnoreProperties
list.
For example, our EF model exposes the Travel class. To make it compatible
with reflection provider, we perform two tweaks. First put the PartitionKey and
RowKey to the DataServiceKey list, and then put EF specific properties such as
EntityState and EntityKey to the IgnoreProperties list, because they cannot be
serialized by DataContractSerializer, and they have no meaning to the clients.
We put the binary GeoLocation property in the IgnoreProperties list as well,
because we don't want to expose it to the clients.
[DataServiceKey(new
string[] {
"PartitionKey", "RowKey" })]
[IgnoreProperties(new
string[] {
"EntityState", "EntityKey",
"GeoLocation" })]
public partial
class Travel
: EntityObject
Then create a service context class which contains a property of type
IQueryable<T>. Since we're using Entity Framework to perform data accessing, we
can simply delegate all data accessing tasks to Entity Framework.
public class
TravelDataServiceContext :
IUpdatable
{
private
TravelModelContainer _entityFrameworkContext;
public IQueryable<Travel>
Travels
{
get
{
return this._entityFrameworkContext.Travels;
}
}
}
Finally, use our own service context class as the generic parameter of the
data service class:
public
class
TravelDataService : DataService<TravelDataServiceContext>
Add CRUD support
In order for a reflection provider to support insert/update/delete, you have
to implement the IUpdatable interface. This interface has a lot of methods.
Fortunately, in most cases, you only need to implement a few of them.
Anyway, first make sure the service context class now implements IUpdatable:
public
class
TravelDataServiceContext : IUpdatable
Now let's walkthrough insert, update, and delete. Note since we're using
Entity Framework to perform data accessing, most tasks can be delegated to EF.
When an HTTP POST request is received, data service maps it to an insert
operation. Once this occurs, the CreateResource method is invoked. You use this
method to create a new instance of the CLR class. But do not set any properties
yet. In our sample, after the object is created, we also add it to the Entity
Framework context:
public object
CreateResource(string containerName,
string fullTypeName)
{
try
{
Type t =
Type.GetType(fullTypeName + ",
AzureBingMaps.DAL", true);
object resource =
Activator.CreateInstance(t);
if (resource is
Travel)
{
this._entityFrameworkContext.Travels.AddObject((Travel)resource);
}
return resource;
}
catch (Exception
ex)
{
throw new
InvalidOperationException("Failed
to create resource. See the inner exception for more details.", ex);
}
}
Then data service iterates through all properties, and for each property,
SetValue is invoked. Here you get the property's name and value deserialized
from ATOM/JSON, and you set the value of the property on the newly created
object.
public void
SetValue(object targetResource,
string propertyName,
object propertyValue)
{
try
{
var property =
targetResource.GetType().GetProperty(propertyName);
if (property ==
null)
{
throw new
InvalidOperationException("Invalid
property: " + propertyName);
}
property.SetValue(targetResource, propertyValue,
null);
}
catch (Exception
ex)
{
throw new
InvalidOperationException("Failed
to set value. See the inner exception for more details.", ex);
}
}
Finally, SaveChanges will be invoked, where we simply delegate the task to
Entity Framework in this case. SaveChanges will also be invoked for update and
delete operations.
public void
SaveChanges()
{
this._entityFrameworkContext.SaveChanges();
}
That's all for insert. Now move on to update. An update operation can be
triggered by two types of requests:
A MERGE request: In this case, the request body may not contain all
properties. If a property is not found in the request body, then it should be
ignored. The original value in the data store should be preserved. But if a
property is found in the request body, then it should be updated.
A PUT request: Where simply every property gets updated.
To simplify the implementation, our sample only takes care of PUT. In this
case, first the original data must be queried. This is done in the GetResource
method. This method is also invoked for a delete operation. The implementation
of this method can be a bit weird, because a query is passed as the parameter,
which assumes a collection of resources will be returned. But during update and
delete, actually only one resource will be queried at a time, so you simply need
to return the first item.
public object
GetResource(IQueryable query,
string fullTypeName)
{
ObjectQuery<Travel>
q = query as
ObjectQuery<Travel>;
var enumerator = query.GetEnumerator();
if (!enumerator.MoveNext())
{
throw new
ApplicationException("Could
not locate the resource.");
}
if (enumerator.Current ==
null)
{
throw new
ApplicationException("Could
not locate the resource.");
}
return enumerator.Current;
}
After GetResource, ResetResource will be invoked, and you can update its
individual properties.
public object
ResetResource(object resource)
{
if (resource is
Travel)
{
Travel updated = (Travel)resource;
var original =
this._entityFrameworkContext.Travels.Where(
t => t.PartitionKey == updated.PartitionKey && t.RowKey ==
updated.RowKey).FirstOrDefault();
original.GeoLocationText = updated.GeoLocationText;
original.Place = updated.Place;
original.Time = updated.Time;
}
return resource;
}
Finally, SaveChanges is invoked, as in the insert operation.
One final operation remaining is delete. This is triggered by a HTTP DELETE
request. It's the simplest operation. First GetResource is invoked to query the
resource to be deleted, and then DeleteResource is invoked. Finally it is
SaveChanges.
public void
DeleteResource(object targetResource)
{
if (targetResource
is Travel)
{
this._entityFrameworkContext.Travels.DeleteObject((Travel)targetResource);
}
}
The above summaries the steps to build a reflection provider for WCF Data
Services. If you want to know more details, we recommend you to read
this comprehensive series by Matt.
Add a custom operation
Our service is now able to expose the data to the world, as well as accept
updates. But sometimes you may want to do more than just data. For example, our
sample exposes a service operation that calculates the distance between two
places. To do so, we can either create a new WCF service, or simply put the
operation in the data service.
To define a custom operation in a data service, you take the same approach as
define an operation in a normal WCF REST service, except you don't need the
OperationContract attribute. For example, we want clients to invoke our
operation using HTTP GET, so we use the WebGet attribute. We don't define a
UriTemplate, thus the default URI will be used:
https://hostname/DataService/TravelDataService.svc/DistanceBetweenPlaces?latitude1=10&latitude2=20&longitude1=10&longitude2=20.
[WebGet]
public double
DistanceBetweenPlaces(double latitude1,
double latitude2,
double longitude1, double longitude2)
{
SqlGeography geography1 =
SqlGeography.Point(latitude1, longitude1,
4326);
SqlGeography geography2 =
SqlGeography.Point(latitude2, longitude2,
4326);
return
geography1.STDistance(geography2).Value;
}
The operation itself uses spatial data to calculate the distance. Recall from
Chapter 4, if a spatial data is constructed for temporary usage, you don't
need a round trip to the database. You can simply use types defined in the
Microsoft.SqlServer.Types.dll assembly. This exactly what we're doing here.
Choose a proper database connection
Recall from
Chapter 3, to design a scalable database, we partition the data horizontally
using PartitionKey. Now let's see it in action.
When creating the Entity Framework context, we use the overload which takes
the name of a connection string as the parameter. We choose a parameter based on
the PartitionKey. In this case, PartitionKey represents the current logged in
user (which will be implemented in later chapters). So it's very easy for us to
select the connection string.
this._entityFrameworkContext =
new
TravelModelContainer(this.GetConnectionString("TestUser"));
///
<summary>
/// Obtain the
connection string for the partition.
/// For now, all
partitions are stored in the same database.
/// But as data and
users grows, we can move partitions to other databases for better scaling.
///
</summary>
private string
GetConnectionString(string partitionKey)
{
return
"name=TravelModelContainer";
}
Test the service
You can test a GET operation very easily with a browser. For example, type
https://hostname/DataService/TravelDataService.svc/Travels in the browser
address bar, and you'll get an ATOM feed for all Travel entities. For POST, PUT,
and DELETE, you can use Fiddler to test them. Atlernatively, you can do as we
do, create a unit test, add a service reference, and test the operations. This
post will not go into the details about unit test. But it is always a good idea
to do unit test for any serious development.
Additional considerations
Since our service will be hosted in Windows Azure, a load balanced
environment, it is recommended to set AddressFilterMode to Any on the service.
[ServiceBehavior(AddressFilterMode
= AddressFilterMode.Any)]
Conclusion
This post discussed how to expose data to the world using WCF Data Services.
In particular, it walked through how to create a reflection provider for WCF
Data Services. The next post will switch your attention to another cloud
service: Bing Maps. We'll create a client application that integrates both Bing
Maps and our own WCF Data Services.