Functions
We’ve been talking a lot about CRUD operations in this chapter and that’s fine: OData was invented for CRUD. However, not everything fits into CRUD. For example, try as I might, I haven’t figure out a good relational model for the digits of pi, although I can tell you, there’s nothing I like doing on a Friday night more than reading through a few hundred of those bad boys and I need to get them from somewhere!
So, for those times when you really want to expose a piece of functionality that isn’t CRUD-related, we have what OData calls “functions” and Data Services calls “service operations,” but it’s really just a way to process a message in the service and return a message in the client.
For example, several jobs back, I was attending a “team-building event,” where somebody got themselves a “Magic Excuse Ball,” which was like a “Magic 8 Ball” except that instead of handing out creepy-accurate answers, it would hand out fun excuses, like “What Memo?” or “Huh?” Of course, I wrote down all those excuses and they’ve been available as a SOAP-based web service from my web site ever since[28]. Now it’s time to redo that as an OData service, which I can do easily with a very simple data model:
[DataServiceKey("Id")] public class Excuse { public int Id { get; set; } public string Text { get; set; } } public class ExcuseContainer { List<Excuse> excuses = new List<Excuse>(new Excuse[] { new Excuse() { Id = 1, Text = "Jury Duty" }, new Excuse() { Id = 2, Text = "Abducted by Aliens" }, ... new Excuse() { Id = 20, Text = "It's Not My Job" }, }); public IQueryable<Excuse> Excuses { get { return excuses.AsQueryable(); } } }
With the data model in place, it’s a simple matter to expose it as a data service:
public class ExcuseService : DataService<ExcuseContainer> { public static void InitializeService(DataServiceConfiguration config) { config.SetEntitySetAccessRule("Excuses", EntitySetRights.AllRead); config.SetServiceOperationAccessRule("GetRandomExcuse", ServiceOperationRights.All); config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2; } [WebGet] public string GetRandomExcuse() { var excuses = CurrentDataSource.Excuses; return excuses.Skip((new Random()).Next(0, excuses.Count())).First().Text; } }
I exposed the Excuses collection reflexively, but it’s really the GetRandomExcuse function that I care about. I’m exposing it via the SetServiceOperationAccessRule just like I expose collections via the SetEntitySetAccessRule. And then, very like an interceptor, I implement my function, this time decorating it with the WebGet attribute, which means that it’s available via the HTTP GET verb. If I wanted to, I could use WebInvoke to support HTTP POST.
The other interesting thing about the implementation is that before the GetRandomExcuse function is called on the service class, the data has already been created and is available in the CurrentDataSource property for you use. The logic to get a random excuse isn’t interesting, but the handy thing about exposing the function for use via HTTP GET is that it can be invoked from the browser (Figure 18).
Figure 18: OData functions in action
You’ll notice that our function name just forms part of the OData URL, just like an entry collection would. Further, functions can return collections and take parameters, as Figure 19 shows.
Figure 19: OData function taking a parameter and returning a collection
Here we’ve called a function called “GetRandomExcuses” and passed in a parameter indicating that we’d like three excuses, please, and be quick about it! The result is a feed document, because we’ve gotten three Excuses entities back, which Data Services knows how to convert to feed entries for us. The definition of the function in the service looks like so:
[WebGet] public IQueryable<Excuse> GetRandomExcuses(int n) { var excuses = CurrentDataSource.Excuses; var result = new List<Excuse>(); var rnd = new Random(); for (int i = 0; i != n; ++i) { result.Add(excuses.Skip(rnd.Next(0, excuses.Count())).First()); } return result.AsQueryable(); }
Notice in this case that we’re taking a single parameter, but functions can take multiple. Notice also that we’re returning an IQueryable, so we can continue to pile on query options, e.g.
http://.../excuseservice.svc/GetRandomExcuses?n=3&$orderby=Text desc
Here we’re sorting the random excuses we get back from our service.
Calling the function from a Data Services client isn’t quite what you’d except, as there’s no methods generated in the service reference proxy class. However, it’s still possible to form the function call yourself:
class Program { static void Main(string[] args) { var service = new ExcuseContainer(new Uri(@"http://localhost:7475/excuseservice.svc")); var getRandomExcuse = new Uri(service.BaseUri + "/GetRandomExcuse"); Console.WriteLine(service.Execute<string>(getRandomExcuse).First()); var getRandomExcuses = new Uri(service.BaseUri + "/GetRandomExcuses?n=3"); var excuses = service.Execute<Excuse>(getRandomExcuses); foreach (var excuse in excuses) { Console.WriteLine(excuse.Text); } } }
This code some look familiar. We generated a service reference from our excuse service, but instead of building LINQ queries, we’re calling the two service operations, forming URLs just like we did in the browser. Calling the service’s Execute function yields a collection of whatever we parameterize the function with. In the first case, we’re getting back one string and in the second, a collection of Excuse objects. Once we have them, we can use them as you’d expect (although clearly our random number generation needs some work, as Figure 20 shows).
Figure 20: Calling an OData function using the WCF Data Services client
In addition, because a function might take arbitrarily long, it can be called asynchronously:
var service = new ExcuseContainer(new Uri(@"http://localhost:7475/excuseservice.svc")); var getRandomExcuses = new Uri(service.BaseUri + "/GetRandomExcuses?n=3"); service.BeginExecute<Excuse>(getRandomExcuses, delegate(IAsyncResult ar) { foreach (var excuse in service.EndExecute<Excuse>(ar)) { Console.WriteLine(excuse.Text); } }, null); Console.WriteLine("Wait for service results, then press [Enter]"); Console.ReadLine();
To kick off an asynchronous operation, we use the BeginExecute method, which takes the query URL just like Execute does. BeginExecute also takes a callback function to call when the operation has been completed and an optional context object. When the callback is called, we turn right back around and call the EndExecute method to return the results, passing in the asynch result object that comes in as a parameter, yielding either the results or, in the case of a failure, an exception.
Figure 21: Calling an OData function asynchronously
Notice in Figure 21 that we kick off the operation first and print the “press [Enter]” message last, but the data from over the network, even on my local machine, is still slower than executing instructions in the current process, which is way the output is ordered as shown.
Footnote
[28] http://sellsbrothers.com/code/excuses.asmx if you must know.