Welcome to MSDN Blogs Sign in | Join | Help

Marcelo's WebLog

Improving the world one entity at a time
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.

http://localhost/WebDataService1.svc/CustomersInLondon

Also, if we look at metadata on http://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:

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

Or navigating through them:

http://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: http://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.': http://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: http://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: http://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: http://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.

Posted: Monday, January 21, 2008 11:06 PM by marcelolr

Comments

Roger Jennings said:

It appears to me that

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);

}

Will return the FirstOrderDate. Isn't "descending" missing?

--rj

# January 22, 2008 10:28 AM

marcelolr said:

Roger, you're correct - this returns the order with the "smallest" date, which is the oldest one in the set, rather than the most recent one. Good catch - I completely missed that when I was trying this out before posting.

Thanks!

# January 22, 2008 12:23 PM

Marcelo's WebLog said:

Speaking of service operations , Mike Taulty has a screen cast on them here - much nicer than my dry

# January 25, 2008 4:25 PM

Marcelo's WebLog said:

Exception handling can be tricky in a distributed system, and I'd like to use this post to show a couple

# January 29, 2008 4:53 PM

Jeremy said:

Great post! - Thanks for these samples. Got me up to speed in less than 30 minutes.

# January 30, 2008 12:14 PM

Marcelo's WebLog said:

Today's entry is about a feature that allows ADO.NET Data Services to play very nicely with other software

# July 14, 2008 2:47 AM

Johan's Avanade Blog said:

I have been working lately with ADO.NET Data Services, and I found several tutorials on how to create...

# October 16, 2008 7:49 PM

davcup's Blog said:

ADO.Net Data Services oltre ad esporre i metodi CRUD per lavorare con le entities, permette anche di

# October 29, 2008 7:02 AM
New Comments to this post are disabled
Page view tracker