четверг, 10 сентября 2009 г.

Доступ к WCF службе одновременно по SOAP и REST (pox & json)

WCF службы позволяют легко менять протокол общения с ними. Одна служба может общаться с разными клиентами по разным протоколам. Т.е. можно использовать как и SOAP так REST механизм общения с одной и той же службой. При этом код службы будет одинаковым, а меняется только интерфейс и файл конфигурации.
Давайте создадим WCF службу, которая позволяет вызывать себя по-разному. Будем поддерживать две методологии доступа: SOAP и REST. Т.е. у нас будет одна служба с тремя точками доступа.
  1. SOAP
  2. REST ответы возвращаются как XML
  3. REST ответы возвращаются как JSON
Служба HomeCatalog должна позволять хранить информацию о домашней коллекции фильмов.
Она будет реализовывать следующие методы:
  • Counts – количество фильмов в каталоге
  • GetFilm – вернуть описания фильма по ID
  • GetFilmNames – вернуть последние добавленных фильмов
  • GetFilmsByGenre – вернуть фильмы по жанру
  • AddFilm – добавить фильм в каталог
Для простоты реализации работы с базой не будет, а фильм может быть только одного жанра.
Создаем новую WCF службу с одним сервисом, который реализует следующий интерфейс:
 
[ServiceContract]
public interface IFilms
{
  [OperationContract] int Count();
  [OperationContract] FilmEntity GetFilm(string filmID);
  [OperationContract] List<string> GetFilmNames();
  [OperationContract] List<FilmEntity> GetFilmsByGenre(FilmGenre genre);
  [OperationContract] void AddFilm(FilmEntity film);
}
[DataContract]
public class FilmEntity
{
  [DataMember] public int ID { get; set; }
  [DataMember] public string Guid { get; set; }
  [DataMember] public string Title { get; set; }
  [DataMember] public int Year { get; set; }
  [DataMember] public FilmGenre Genre { get; set; }
  [DataMember] public string Description { get; set; }
}
[DataContract]
public enum FilmGenre
{
  [EnumMember] Action,
  [EnumMember] Comedy,
  [EnumMember] Drama,
  [EnumMember] War
}
Реализация службы:
public class Films : IFilms
{
  private readonly SortedDictionary<string, FilmEntity> _films;
  public Films()
  {
    _films = GetFilms();
  }
  public int Count()
  {
     return _films.Count;
  }
  public FilmEntity GetFilm(string filmGuid)
  {
    return _films[filmGuid];
  }
  public List<string> GetFilmNames()
  {
    List<string> filmNames = new List<string>(_films.Count);
    foreach (FilmEntity film in _films.Values)
    {
      filmNames.Add(film.Title);
    }
    return filmNames;
  }
  public List<FilmEntity> GetFilmsByGenre(FilmGenre genre)
  {
    List<FilmEntity> filmsByGenre = new List<FilmEntity>(_films.Count);
    foreach (FilmEntity film in _films.Values)
    {
     if(film.Genre == genre)
     {
      filmsByGenre.Add(film);
     }
    }
   return filmsByGenre;
  }
  public void AddFilm(FilmEntity film)
  {
    if(string.IsNullOrEmpty(film.Guid))
    {
     film.Guid = Guid.NewGuid().ToString();
    }
   _films.Add(film.Guid, film);
   }
 
  private SortedDictionary<string, FilmEntity> GetFilms()
  {
   SortedDictionary<string, FilmEntity> films = new SortedDictionary<string, FilmEntity>();
   //fill test data here
   return films;
  }

  }
Файл конфигурации службы:
<system.serviceModel>
<services>
<service behaviorConfiguration="HomeCatalogService.FilmsBehavior" name="HomeCatalogService.Films">
<endpoint address="" binding="wsHttpBinding" contract="HomeCatalogService.IFilms" />
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
service>
services>
<behaviors>
<serviceBehaviors>
<behavior name="HomeCatalogService.FilmsBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" />
behavior>
serviceBehaviors>
behaviors>
system.serviceModel>
Сервис нужно захостить на ISS.
Теперь у нас есть WCF сервис с которым можем общаться по SOAP протоколу, в этом не должно быть ничего сложного. Для проверки работы службы сделаем тестовый консольный клиент, который используя автоматически сгенерированный прокси класс общается с сервисом.
Вот код клиента:
CatalogServiceReference.FilmsClient client = new CatalogServiceReference.FilmsClient();
Console.WriteLine("Count of films: {0}", client.Count());
string[] filmNames = client.GetFilmNames();
Console.WriteLine("Films:");
foreach (string name in filmNames)
{
Console.WriteLine(name);
}
Далее начинается более интересное, нам нужно научить нашу службу работать по REST архитектуре. Для начала нужно добавить в сервисе reference System.ServiceModel.Web. После это нужно добавить атрибуты WebGet или WebInvoke ко всем методам в интерфейсе. Теперь интерфейс будет выглядеть так:
[ServiceContract]
public interface IFilms
{
[OperationContract]
[WebGet]
int Count();
[OperationContract]
[WebGet]
FilmEntity GetFilm(string filmID);
[OperationContract]
[WebGet]
List<string> GetFilmNames();
[OperationContract]
[WebGet]
List<FilmEntity> GetFilmsByGenre(FilmGenre genre);
[OperationContract]
[WebInvoke]
void AddFilm(FilmEntity film);
}
Теперь осталось изменить web.config – добавить точки доступа(endpoint) для REST и настроить их поведение для возврата данных как XML или JSON.
<system.serviceModel>
<services>
<service behaviorConfiguration="HomeCatalogService.FilmsBehavior" name="HomeCatalogService.Films">
<endpoint address=""
binding="wsHttpBinding"
contract="HomeCatalogService.IFilms" />
<endpoint address="pox"
binding="webHttpBinding"
behaviorConfiguration="poxBehavior"
contract="HomeCatalogService.IFilms" />
<endpoint address="json"
binding="webHttpBinding"
behaviorConfiguration="jsonBehavior"
contract="HomeCatalogService.IFilms" />
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
service>
services>
<behaviors>
<serviceBehaviors>
<behavior name="HomeCatalogService.FilmsBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" />
behavior>
serviceBehaviors>
<endpointBehaviors>
<behavior name="poxBehavior">
<webHttp/>
behavior>
<behavior name="jsonBehavior">
<enableWebScript/>
behavior>
endpointBehaviors>
behaviors>
system.serviceModel>
Теперь к нашему сервису можно обратиться через http запросы, к примеру так:
http://localhost/HomeCatalogService/Films.svc/pox/Count
http://localhost/HomeCatalogService/Films.svc/pox/GetFilmNames
С методом AddFilm немного сложнее, тут нужно формировать POST запрос или переключить клиент с SOAP на REST.
Исходные коды примеров:
Только soap
SOAP и REST
Исходный код примеров

вторник, 1 сентября 2009 г.

Открытие

В рамках решения сложной задачи, я часто создаю тестовые проекты. В последнее время я много работаю с Windows Communication Foundation (WCF) и пришлось решить несколько интересных задач. Хотелось бы поделиться найденными мною решениями.