Condividi tramite


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

https://blogs.msdn.com/b/windows-azure-support/archive/2010/08/11/bring-the-clouds-together-azure-bing-maps.aspx.


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 as

    this 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.