Taking advantage of OData with ADAM

When there’s a need to publish information from a web application in a manner that can be consumed by client applications easily, Web Services have long been a logical choice. Historically, there’s been a number of options that can be considered. XML-RPC and SOAP, in their different forms with different levels of complexity and vendor support exist already for a long time now, but they usually meant having to spend a decent amount of effort in order to make them useful, both from the viewpoint of the producer as from the consumer. As a result, in later years different concepts such as REST and JSON have become a more popular option in situations where the consumer is the client-side programming in browsers. But each and every service has their own idea of how to present that information, usually geared towards its own client-side code, with no clear standard for different services to interoperate.

Enter OData. As an open standard sponsored by Microsoft and adopted by giants such as FaceBook and Netflix, OData promises to “provide an easy way to break down data silos and increase the shared value of data.”. It builds upon established standards like ATOM and JSON and provides a way consumers on various platforms to query and update information made accessible from different platforms. Have a look at their website, and read the protocol overview.

In this tutorial, I will explain how one can write a simple OData service in .NET 3.5 SP1 that exposes information stored in ADAM.

In order to truly appreciate OData, one only needs to understand what it’s basically about: querying and updating data, as simple as possible. There’s an OData client for most popular development tools, including .NET, JavaScript, PHP, Ruby, Java, Objective-C, Silverlight, but in the end all you need is a way to fetch XML or JSON from the web and you’ll be fine. Here’s what you need:

  • LINQPad, which works great as an OData client. There’s a good article that demonstrates how LINQPad can be used to query StackOverflow.
  • Visual Studio 2008 SP1, which includes .NET 3.5 SP1.
  • A working ADAM database. Like in my previous MVC article, you’ll want to avoid a production database.
  • the Data Services update for .NET 3.5 SP1 for your platform. For Windows 7 or Windows Server 2008 R2, go here, otherwise go here.

I’ll be using LINQPad in this example as a client, but you can use any OData visualizer out there. There’s even a free one for your iPhone. The database that I’m using, AdamTV, is the one that I created for my presentation during Community Day. It’s basically a bunch of movies, organized as follows:

  • There’s a classification named “Genres” (Identifier: AdamTV_Genres) under which I have a classification for every genre that I wish to classify a movie under. A movie can subsequently be classified under more than one genre.
  • There’s a record for each Movie, and that record is also classified under a classification named “Movies” (Identifier: AdamTV_Movies) so that every Movies has the following fields: Title, Description, License, Keywords and a couple others.

There’s a bit more to it, but this is all we need for this demonstration. We’ll be building a WCF Data Service, but in VS2008 this is still referred to as ADO.NET Data Service, even tough there’s no requirement to use ADO.NET as your data source.

To start, open up VS2008 and create a new WCF Service Application project. If you want to follow the client tutorial as well, you should make sure that ‘Create directory for solution’ is checked, so you add a console application later.

Delete the service that is created by default (Service1.svc and IService1.cs), then add a new ADO.NET Data Service item. Mine is named ‘Movies.svc’.

This is what my solution looks like so far:

Since this a WCF Service, we’ll want to abstract the ADAM API away a bit and give the clients a small subset of the information that is present. What we want to make is a Service Model that represents what we want our clients to see, not the entire ADAM object model. We’ll keep this as simple as possible and make a new folder in our service project called ‘Model’, and in it we’ll put two very simple classes: Genre.cs and Movie.cs.

C#
1
2
3
4
5
6
7
8
9
10
11
12
using System;
using System.Data.Services.Common;

namespace AdamTV.OData.Model
{
    [DataServiceKey("Id")]
    public class Genre
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
    }
}
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using System.Collections.Generic;
using System.Data.Services.Common;

namespace AdamTV.OData.Model
{
    [DataServiceKey("Id")]
    public class Movie
    {
        public Guid Id { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public string License { get; set; }
        public IList<Genre> Genres { get; set; }
    }
}

WCF Data Service can work with the Entity Framework provider, if you have an Entity Framework model, or it can work with the Reflection provider, which works with IQueryable<T>. You can also write your own provider, but that’s a bit more work that I think is best left for another time. The idea behind the Reflection provider is that you have your own entities, which are plain .NET types, and then there’s a class that ‘exposes’ those entities as properties of IQueryable<T>. The Reflection provider will then use reflection on your types to determine what the service looks like to the client and will hand all querying.

Thus we need a MovieData type, in lack of a better term, that exposes two properties: Movies and Genres, like this:

C#
1
2
3
4
5
public class MovieData
{              
    public IQueryable<Genre> Genres { get; }
    public IQueryable<Movie> Movies { get; }
}

Of course, this doesn’t include an implementation yet, because we need an ADAM application first. To do that, add references to the Adam.Core and Adam.Tools assemblies. Then, add the following members to the MovieData class:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private const string RegistrationName = "AdamNet";
private const string ServiceUserName = "Administrator";
private const string ServicePassword = "password";

private static Guid _SessionId;
private static readonly object SessionIdLock = new object();

private readonly Application _Application;

public MovieData()
{
    this._Application = new Application();

    lock(SessionIdLock)
    {
        if (_SessionId != Guid.Empty)
        {
            LogOnStatus status = this._Application.LogOn(RegistrationName, _SessionId);
            if (status != LogOnStatus.LoggedOn)
            {
                _SessionId = Guid.Empty;
            }
        }

        if (_SessionId == Guid.Empty)
        {
            LogOnStatus status = this._Application.LogOn(RegistrationName, ServiceUserName, ServicePassword);
            if (status != LogOnStatus.LoggedOn)
            {
                string message = string.Format("Authentication failed: {0}.", status);
                throw new ApplicationException(message);
            }
        }
    }
}

This is so we can authenticate with ADAM without required the client to authenticate. You’ll want to change the constants to more sensible values. Also note that this technique will only work with our ADAM Enterprise license, since there’s a concurrency limit with ADAM Corporate licenses.

Next we’ll implement those property getters. With the help of LINQ and a few helper methods, we can keep this relatively short:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
private static Genre ClassificationToGenre(Classification classification)
{
    return new Genre
    {
        Id = classification.Id,
        Name = classification.Name
    };
}

private static IEnumerable<Genre> GetGenresForRecord(Record record)
{
    ClassificationCollection collection = new ClassificationCollection(record.App);
    collection.Load(new SearchExpression("Parent.Identifier = AdamTV_Genre and Id in ?",
        new object[] { record.Classifications.CopyClassificationIdsToArray() }));
    return collection.Cast<Classification>().Select(item => ClassificationToGenre(item));

}

public IQueryable<Genre> Genres
{
    get
    {
        ClassificationCollection collection = new ClassificationCollection(this._Application);
        collection.Load(new SearchExpression("Parent.Identifier = AdamTV_Genre"));

        var result = collection.Cast<Classification>().Select(item => ClassificationToGenre(item));

        return result.AsQueryable();                
    }
}

public IQueryable<Movie> Movies 
{ 
    get
    {
        RecordCollection collection = new RecordCollection(this._Application);
        collection.Load(new SearchExpression("Classification.Identifier = AdamTV_Movies"));

        var result = collection.Cast<Record>().Select(record => new Movie()
        {
            Id = record.Id,
            Title = record.Fields.GetField<TextField>("AdamTV_Title").Value,
            Description = record.Fields.GetField<TextField>("AdamTV_Description").Value,
            License = string.Join(", ", record.Fields.GetField<OptionListField>("AdamTV_License").Items.Cast<OptionListItem>().Select(item => item.Definition.Name).ToArray()),
            Genres = GetGenresForRecord(record).ToList()                                                     
        });

        return result.AsQueryable();                        
    }
}

At this point all that’s left to do to get this working is to set the name of the data source in the service, and apply some rules to provide read access to the entity sets.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System.Data.Services;
using AdamTV.OData.Model;

namespace AdamTV.OData
{
    public class Movies : DataService<MovieData>
    {
        // This method is called only once to initialize service-wide policies.
        public static void InitializeService(IDataServiceConfiguration config)
        {
            config.SetEntitySetAccessRule("Genres", EntitySetRights.All);
            config.SetEntitySetAccessRule("Movies", EntitySetRights.All);
            config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
        }
    }
}

To see if everything works OK, run it. Use your browser to go to the service’s url:

This is a good indication that the service is running. Now fire up LINQPad, and click ‘Add connection’ in the list of connections. A dialog will pop up. Here you select the WCF Data Services driver. In the next dialog, fill in the url of the service, and you should now have a new connection in the connection list. When you open that up, you’ll find the LINQPad has found out about the entities that you publish

Now you can start querying.

Or, using the free OData browser for iPhone, you can browse the entities as well.

LINQPad allows you to see the url that it creates from the specified LINQ query, by going to the "SQL" tab (yes, it makes sense). If you copy & paste that url into your browser you'll be greeted with an ATOM feed. When you make the same request from AJAX, you'll get a JSON object.

Another thing to note is that with the Reflection provider, LINQ is used to pass query parameters. In our case, this happens after we loaded all the assets from the ADAM system and then the objects are filtered by LINQ-to-Objects, so that’s pretty inefficient. One solution is to write a custom data provider.

Now, to create a client, add a console application. In it, add a Service Reference and fill in the url for the service. Also give the namespace some meaningful name.

Then, browsing is as easy as this:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;

namespace AdamTV.OData.TestClient
{
    class Program
    {
        static void Main(string[] args)
        {
            var movieData = new Movies.MovieData(new Uri("http://localhost/AdamTV.OData/Movies.svc/"));

            foreach (var movie in movieData.Movies)
            {
                Console.WriteLine("Movie: {0}", movie.Title);
            }
        }
    }
}

Comments

Leave a comment
You must be logged in to post comments.
Sign in now
 
 
Technical
Business
rss feed