This is the sixth article in our "Bring the clouds together: Azure + Bing Maps" series. You can find a preview of live demonstration on http://sqlazurebingmap.cloudapp.net/. For a list of articles in the series, please refer to http://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 http://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 http://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: http://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 http://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.