Service Operations in ADO.NET Data Services

One way in which you can expose additional resources from your ADO.NET Data Service is to implement "service operations" on your WebDataService subclass.

For example, let's say we want to return all customers in a given city in a pre-baked entry point. We can write this code on the server:

public class WebDataService1 : WebDataService< Model.Entities >
{
public static void InitializeService(IWebDataServiceConfiguration config)
{
config.SetResourceContainerAccessRule("Customers", ResourceContainerRights.AllRead);
config.SetServiceOperationAccessRule("CustomersInLondon", ServiceOperationRights.All);
}

[WebGet]
public IQueryable<Model.Customers> CustomersInLondon()
{
return from c in this.CurrentDataSource.Customers
where c.City == "London"
select c;
}
}

Here's what's going on:

  • In InitializeService, we make sure that the resource set we have is visible.
  • Next, we make sure that the service operation we have is also visible.
  • We declare a method, CustomersInLondon, which returns an IQueryable of customers.
  • We add an attribute to the method, WebGet, which indicates we want to allow GET operations on this method.
  • Finally, we return the query we're interested in. Now that there is a property CurrentDataSource, of type Model.Entities (the 'T' in WebDataService<T>), which we can use as the querying context.

Now we can hit F5 and run our project, and navigate to the following URL to get customers from London.

https://localhost/WebDataService1.svc/CustomersInLondon

Also, if we look at metadata on https://localhost/WebDataService1.svc/$metadata, we'll see the following bit of CSDL:

<FunctionImport Name="CustomersInLondon" EntitySet="Customers" ReturnType="Collection(Model.Customers)" />  

Note that because we're returning an IQueryable, we can keep composing over the returned results, for example by filtering them:

https://localhost/WebDataService1.svc/CustomersInLondon?$filter=ContactTitle%20eq%20'Sales%20Manager'

Or navigating through them:

https://localhost/WebDataService1.svc/CustomersInLondon('AROUT')/Orders

Let's say that we didn't want to allow clients to do this - instead, we want to return a very "locked down" set of results. In that case, we simply need to change our method to return an IEnumerable result rather than an IQueryable. Note that even if the actual type is an IQueryable, as long as the result type is declared as an IEnumerable, the server won't allow composition over it.

[WebGet]
public IEnumerable<Model.Customers> CustomersInLondon()
{
return from c in this.CurrentDataSource.Customers
where c.City == "London"
select c;
}

So this URL still returns results as expected: https://localhost/WebDataService1.svc/CustomersInLondon

But this fails with a message 'Query options $expand, $filter, $orderby, $skip and $top cannot be applied to the requested resource.': https://localhost/WebDataService1.svc/CustomersInLondon?$filter=ContactTitle%20eq%20'Sales%20Manager'

Let's say that now want to allow the customers to tell us which specific city they're interested in, rather than hard-coding the city name.

We can add the following method to our class to enable this.

...
public static void InitializeService(IWebDataServiceConfiguration config)
{
...
config.SetServiceOperationAccessRule("CustomersInCity", ServiceOperationRights.All);
}

[WebGet]
public IQueryable<Model.Customers> CustomersInCity(string cityName)
{
return from c in this.CurrentDataSource.Customers
where c.City == cityName
select c;
}

Now we can get results for this operation with the following URL: https://localhost/WebDataService1.svc/CustomersInCity?cityName='London'

As you can see, the syntax is simply the parameter name, an equals, and the value literal as you would have used in a filter or a key. The only parameter types currently supported are primitive types (numbers, string, DateTime, byte array, Guid). This is in following the the form encoding used by web browser for FORM tags.

If you want to pass parameters in the request body rather than the query portion of the URL, you can use WebInvoke on the service operation method instead of WebGet. The clients then use POST rather than GET, and pass the queries in the body of the request, using application/x-www-form-urlencoded encoding.

There are two more attributes that are interesting for service operations. You can use the Microsoft.Data.Web.SingleResultAttribute attribute to indicate a single result will be returned from a specific operation.

...
public static void InitializeService(IWebDataServiceConfiguration config)
{
...
config.SetServiceOperationAccessRule("LastOrderDate", ServiceOperationRights.All);
}

[SingleResult]
[WebGet]
public IQueryable<DateTime> LastOrderDate()
{
var result =
from o in this.CurrentDataSource.Orders
orderby o.OrderDate
select o;
return result.Take(1).Select((o) => o.OrderDate.Value);
}

You can now access this value from the following URL: https://localhost/WebDataService1.svc/LastOrderDate

And finally, the MimeTypeAttribute can be returned to indicate that a given MIME type applies to the returned value of an operation.

...
public static void InitializeService(IWebDataServiceConfiguration config)
{
...
config.SetServiceOperationAccessRule("WelcomePage", ServiceOperationRights.All);
}

[MimeType("text/html")]
[SingleResult]
[WebGet]
public IQueryable<string> WelcomePage()
{
return new string[]
{
"<html><head><title>Welcome</title></head>" +
"<body><h1>Welcome!</h1><p>Currently we have " +
this.CurrentDataSource.Customers.Count().ToString() +
" customers.</p></body></html>"
}.AsQueryable();
}

You can now access this information through the following URL: https://localhost/WebDataService1.svc/WelcomePage/$value

Now, there are far better methods for generating web pages, but I just wanted to give a little taste of how having a (simple!) uniform interface for accessing resources allows you to integrate disparate systems, like a web browser and an ADO.NET Data Service.

 

This post is part of the transparent design exercise in the Astoria Team. To understand how it works and how your feedback will be used please look at this post.