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.

Comments (9)

  1. 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

  2. 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!

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

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

  5. Jeremy says:

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

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

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

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

Skip to main content