-
Notifications
You must be signed in to change notification settings - Fork 5
Home
(http://lostintangent.tumblr.com/post/3189655590/you-want-to-wrap-odata-around-what)
In a nutshell: the WCF Data Services Toolkit makes it easier for you to “wrap” any type of data (or multiple data sources) into an OData service. In practice, many people have done this with existing web APIs, XML files, etc. As mentioned on the Codeplex page, this toolkit is what is running many of the OData services out there (e.g. Facebook, Netflix, eBay, Twitpic).
Because the OData consumption story and audience is pretty deep, having data exposed via that protocol can provide a lot of benefits (even as a secondary API option) for both developers and end-users. If compelling data exists, regardless what shape and/or form it’s in, it could be worthwhile getting it out there as OData, and the toolkit strives to make that process easier.
For example, if you have interesting data already being exposed via a web API, and you’d like to layer OData on top of that (perhaps to prototype a POC or get feedback from customers without making a huge technical investment), you would have a lot of work to do in order to get that working with the currently available bits. With the WCF Data Services Toolkit it’d be reasonably simple*. To illustrate how this would look, let’s take a look at an example of how to build such a solution.
- Knowledge of WCF Data Services and overall web APIs is still required.
One of my favorite products as of late is Instagram. They’ve traditionally been solely an iPhone product, but they’ve fully embraced the “Services Powering Experiences" mantra and released an API so that other clients can emerge, making them a service provider, not just an iPhone application developer. I’d love to see what it would look like to have an OData version of that API, so we’ll use the toolkit to proof that out.
Step #1: Define your model
The Instagram model includes entities like Media, Location, Tags, etc. but we’re going to keep things simple and just focus on the User entity from their API. Obviously, a User is someone who has signed up for Instagram and can submit images, follow users, and also be followed by other users. A simple C# declaration of that entity could look like so:
[DataServiceKey("Id")]
public class User
{
public string Id { get; set; }
public string UserName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public User[] Follows { get; set; }
public User[] Followers { get; set; }
public string AvatarUrl { get; set; }
}
Notice that I’m specifying navigations (e.g. Follows, Followers) as simple CLR properties, which makes it easy to define relationships between entity types. Also note that I’m using the DataServiceKeyAttribute (from WCF Data Services) to declare what my key property is. [Every entity type in WCF Data Services has to have a key, for obvious reasons]. This should all be very intuitive so far.
Step #2: Define your context
Once you have your entity types, you need to define your context, that declares the name and types of all collections your OData service will expose. This concept isn’t new to the toolkit, so it shouldn’t come as a surprise. The only difference here is where your data is coming from.
public class InstagramContext : ODataContext
{
public IQueryable<User> Users
{
get { return base.CreateQuery<User>(); }
}
public override object RepositoryFor(string fullTypeName)
{
if (fullTypeName == typeof(User).FullName)
return new UserRepository();
return null;
}
}
Notice that my context is derived from ODataContext which is a special class within the toolkit that knows how to retrieve data using the “repository style” we’ll be using here (more later). I declare a collection called “Users” that will return data of type User that we defined above. We don’t have to worry about how to create an IQueryable that actually handles that (very complex task), but rather we can use the CreateQuery method of the ODataContext class.
Where the actual “magic” happens is within the RepositoryFor method that we overrode from the ODataContext class. This method will be called whenever a query is made for data of a specific type. In our case, whenever a query is made for User data, we need to return a class to the runtime that will know how to actually get User data. Here we’ve called it UserRepository.
Step #3: Define your repository
The toolkit expects you to give it a “repository” that will actually serve your data. The word “repository” here is used in the loosest sense of the term. All it really needs is a class that follows a couple simple conventions. Here is what the UserRepository class could look like:
public class UserRepository
{
public User GetOne(string id)
{
var url = "https://api.instagram.com/v1/users/" + id + "?client_id=" + InstagramSettings.ClientId;
var client = new WebClient();
var userString = client.DownloadString(url);
var user = Json.Decode(userString).data;
return new User
{
Id = user.id,
UserName = user.username,
FirstName = user.first_name,
LastName = user.last_name,
AvatarUrl = user.profile_picture
};
}
}
Note: The InstagramSettings.ClientId property is simply my registered application’s ID. I’m omitting the actual value so as to not get bombarded with calls from anyone besides me.
Notice that this is a POCO class (no base class or attributes) with a single method called GetOne. This method will be called by convention (doesn’t need to be wired up anywhere) by the runtime when a request is made for a specific User, such as /Users(‘55’). To make matters even simpler, our method can declare the key parameters that will be passed in, that way we don’t have to deal with the actual HTTP request, and more importantly, any of the expression tree that is built from the OData request. We simply are given an ID and asked to return a user.
To do that, I make a very trivial call to the Instagram API, decode the JSON, and re-shape it into my version of the User entity. And that’s it. A handful of boilerplate code, and some basic knowledge of the Instagram API and I’ve got a basic OData service wrapping their data.
Step #4: Define your service
Now that we have our entity, context and repository, we just need to define our OData service that will be the actual endpoint for users to hit. Once again, this is a purely WCF Data Services concept, and the code will look identical to what you’d expect.
public class InstagramService : ODataService<InstagramContext>
{
public static void InitializeService(DataServiceConfiguration config)
{
config.SetEntitySetAccessRule("*", EntitySetRights.All);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
}
}
The only point of interest here is that we’re deriving from ODataService and not DataService. ODataService is a supertype of DataService and provides a handful of functionality on top of it (e.g. JSONP, output caching). It’s part of the toolkit, and you don’t have to use it, but you mine as well since you’ll get more features.
With this code in place, if I run my service and hit the root URL, I’ll get the following:
[image]
Notice my Users collection as expected. If I request a specific User by ID (/Users(‘55’), then I should get back that User (trimmed):
[image]
I can also request a specific property for a User like always (/Users(‘55’)/FirstName or even /Users(‘55’)/FirstName/$value) and that will work too.
Great, now let’s add the ability to get multiple users (as a feed) in addition to just one user. To do this, let’s add another method to the UserRepository class, once again following a simple convention.
public IEnumerable<User> GetAll(ODataQueryOperation operation)
{
if (!operation.ContextParameters.ContainsKey("search"))
throw new NotImplementedException("Users cannot be enumerated, they can be accessed by using the User's ID. e.g. /Users('55') or by providing a 'search' query string.");
var searchString = operation.ContextParameters["search"];
var url = "https://api.instagram.com/v1/users/search?q=" + searchString + "&client_id=" + InstagramSettings.ClientId;
var client = new WebClient();
var usersString = client.DownloadString(url);
var usersJson = Json.Decode(usersString).data;
var list = new List();
foreach (var user in usersJson)
list.Add(ProjectUser(user));
return list;
}
Instead of GetOne, the convention for retrieving a feed of entities is to create a method called GetAll. This method could take no parameters, but to illustrate another feature of the toolkit, I’ve declared a single parameter of type ODataQueryOperation. This is a simplified view of the incoming OData request (e.g. filters, ordering, paging) that is constructed for you to easily “reach into”, once again, without having to parse the URL or deal with any expression trees. Just by declaring that parameter, we’ll now have access to all of that data.
Because retrieving a list of all users wouldn’t be very useful (or practical), if a request is made for th Users feed, we’ll return an error saying that isn’t allowed. What we do allow though is for a search criteria to be specified, at which point, we’ll pass that search string to the Instagram API, and then project the returned JSON response into our User entity type. The code is virtually identical to our GetOne method.
If we re-run the service and hit the Users feed (/Users), we’ll get an error as expected. If we add a search criteria though (/Users?search=jon), we’ll get back a feed of all users with “jon” in their username, first name or last name.
[image]
Note: The search semantics we’re using here isn’t specific to OData, it’s a feature we added onto the service. Because the toolkit provides the ContextParameters dictionary in the query operation, we can easily grab any “special” query string values we allow our users to provide.
Up to this point we’ve only worked with scalar properties, which is interesting, but not nearly as cool as having rich navigations. OData really shines when exposing hierarchical data, such as the association of a User being able to have a list of followers and be able to also follow a list of users.
The Instagram API doesn’t return that associated data when you request a user, so you’d have to call another service endpoint. In order to let the WCF Data Services Toolkit runtime know that an entity association requires “special” treatment, you need to annotate it with a ForeignPropertyAttribute, like so:
[DataServiceKey("Id")]
public class User
{
// Other properties omitted for brevity...
[ForeignProperty]
public User[] Follows { get; set; }
[ForeignProperty]
public User[] Followers { get; set; }
}
(todo)