Способы расширения функциональности ADO.NET Data Services с помощью сервисных операций

Как мы знаем ADO.NET Data Services предоставляет нам удобный синтаксис URI, с помощью которого можно адресовать любые части модели данных. Кроме того, конструируя URI мы можем выполнять какие-то серверные операции вроде сортировки, фильтрации или разбиения на страницы. Но как же быть со сложными запросами, которые, например, включают в себя несколько join-ов?

При решении этой задачи можно конечно сделать несколько обращений к сервису, получить нужные данные, а затем построить свой сложный запрос поверх in-memory объектов, но есть способ лучше :).

В рамках сервисов ADO.NET Data Services мы можем создать специальные методы, которые называются сервисные операции.

[WebGet]
public IQueryable<Customers> GetCustomersWithOrders()
{
  return ...;
}

Как мы видим у этого метода есть две особенности: во-первых, мы помечаем его атрибутом WebGet или WebInvoke; а во-вторых, для того, чтобы это была полноценная коллекция данных мы должны вернуть интерфейс IQueryable.

Теперь, когда в рамках сервиса имеется такой метод мы можем совершенно стандартно обратиться к нему следующим образом:

http://../Service.svc/GetCustomersWithOrders/

Точно также мы можем создать операцию с параметрами.

[WebGet]
public IQueryable<Customers> GetCustomersWithOrders(int count)
{
  return ...;
}

В этом случае можно обратиться к этой операции так:

http://../Service.svc/GetCustomersWithOrders/?$count=25

Понятно, что в рамках этой операции мы можем строить сколько угодно сложный запрос, включая join-ы и т.д. При этом, используя свойство CurrentDataSource мы можем обратиться к источнику данных, на основе которого построен сам сервис. Таким образом, метод может выглядеть следующим образом:

[WebGet]
public IQueryable<Customers> GetCustomersWithOrders(int count)
{
  var q = from c in this.CurrentDataSource.Customers
          where c.Orders.Count > count
          select c;

  return q.AsQueryable();
}

В данном случае мы создаем LINQ запрос к источнику данных, используя возможно какие-то параметры, join-ы, группировки, фильтровки и др. и возвращаем результат в виде IQueryable.

В этих случаях мы можем осуществлять GET-запросы к указанным операциям. Если мы хотим использовать методы, например, PUT, POST или любые другие, то мы должны использовать атрибут WebInvoke, вместо WebGet.

[SingleResult]
[WebInvoke(Method="POST")]
public void DoSomething()
{
   // ...
}

При этом мы можем указать абсолютно разнообразные методы при указании параметров атрибута.

Ну и на самом деле мы можем вовзращять не обязательно коллекцию объектов, можно вернуть например строку. При этом мы должны пометить операцию атрибутом SingleResult. Если необходимо, чтобы сервис корретно выдавал HTTP-заголовок ContentType, то нужно пометить класс сервиса атрибутом MimeType и в нем указать к какой метод какой тип информации должен возвращать. В итоге можем получить следующую операцию:

[MimeType("Help", "text/html")]
public class NorthwindService : DataService<NorthwindEntities>
{
   [SingleResult]
   [WebGet]
   public string Help()
   {
      return @"<html>...</html>";
   }

  // ...
}

Кстати, не обязательно возвращать строку, можно вернуть например бинарный поток, в этом случае в качестве типа возвращаемого значения будет Stream.

Однако, с операциями, которые возвращают не IQueryable существует одна досадная неприятность. Дело в том, что если мы вернем например строку, и обратимся к этому методу как обычно, то результат будет обернут в XML. Для того, чтобы получить тот же самый HTML, который мы хотим получить, необходимо обратиться к операции, используя ключевое слово $value.

http://../Service.svc/Help/$value

Теперь, на выходе мы получим HTML-код и заголовок ContentType будет содержать правильное значение. Точно также дела обстоят с бинарными данными, например картинками.