WCF RIA Services для жизни
    Сообщений 1    Оценка 105 [+1/-0]         Оценить  
Система Orphus

WCF RIA Services для жизни

Автор: Лепешенков Константин Евгеньевич
Опубликовано: 21.05.2012
Исправлено: 10.12.2016
Версия текста: 1.1

Введение
Экскурс в историю
Программирование с помощью мыши
Как это устроено
1. Базовые классы для сервисов доступа к данным
2. Кодогенератор
3. Базовые классы для сущностей и контекстов на клиенте
4. Специализированная реализация WCF-сервиса
Немного кода
Модель данных приложения RIABlog
Реализация связей «один-ко-многим»
Работа с иерархией сущностей
Работа с клиентским контекстом
Перегрузка кодогенератора
Вопросы безопасности
Имеем на выходе
Список литературы

Исходные коды к статье

Введение

В русском Интернете можно найти немало статей и руководств на тему разработки под Silverlight, а вот на тему применения в реальных проектах его дополнения – технологии WCF RIA Services – русскоязычной информации практически нет. В этой статье я попытаюсь свести воедино свой опыт применения этого фреймворка и надеюсь предложить читателям больше информации, чем можно почерпнуть из официальных “HOWTO” и “Walkthrough”.

Экскурс в историю

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

Попытка ребят из Microsoft пошатнуть монополию Adobe в области так называемых Rich Internet Applications (RIA), как известно, провалилась. Платформа Silverlight так и не смогла завоевать сердца дизайнеров и по-прежнему встречается в Сети только на сайтах, так или иначе аффилированных с Редмондом. Делать нечего, пришлось искать для Silverlight другую продуктовую нишу, и она нашлась. Оказывается, на этой платформе удобно создавать пользовательские интерфейсы для всевозможных корпоративных информационных систем. Получается нечто среднее между «тонким» и «толстым» клиентом, но без винегрета из ASP.Net, HTML и JavaScript и, в то же время, без необходимости регулярного накатывания обновлений на тысячи рабочих машин.

И здесь обнаружилась главная сложность. В корпоративных бизнес-приложениях между клиентом и сервером обычно гоняется огромное количество данных, причем не простых, а структурированных, представляющих модель данных предметной области. Как организовать передачу этих данных? Ведь из Silverlight-приложения к SQL Server напрямую не подключишься!

Конечно, еще в первой версии Silverlight присутствовали базовые средства взаимодействия с web-сервисами, которые постоянно улучшались и к версии 3.0 обрели почти все способности «взрослой» реализации WCF в .Net Framework. Однако необходимость реализовывать обмен данными между клиентами и сервером с помощью дополнительного слоя web-сервисов играла не в пользу Silverlight в его конкуренции с другими, «классическими» архитектурами бизнес-приложений – «толстыми» клиентами (например, на WinForms) и web-приложениями (например, на ASP.Net), поскольку разработка этого дополнительного слоя требовала дополнительных затрат и привносила дополнительные ошибки.

Назревала необходимость этот процесс создания слоя web-сервисов автоматизировать. К этому времени уже приобрели широкую популярность разные средства ORM (Object-Relational Mapping), такие как NHibernate, LINQ2SQL и Entity Framework, они стали все больше теснить устаревшие нетипизированные датасеты в области построения уровней доступа к данным. И тогда уже стала практически очевидной идея сделать так, чтобы объект-контекст доступа к данным (он присутствует почти во всех ORM-фреймворках, можно еще назвать его фасадом для модели данных) умел сам себя сериализовать и синхронизировать изменения между клиентом и сервером. Так и родилась технология WCF RIA Services, которая в виде альфа-версии стала доступна в 2010 году вместе с Silverlight 4, а на момент написания статьи имеет официальную версию V1.0 SP2 и входит в состав Silverlight 5 Tools.

Программирование с помощью мыши

В качестве первого шага давайте быстро, без использования клавиатуры, создадим пробное Silverlight-приложение, получающее данные с сервера с помощью WCF RIA Services. Главная практическая ценность этого упражнения – убедиться, что все необходимые нам инструменты установлены и работают правильно. Читатели, знакомые с web-кастами на silverlight.net и/или с programming guide в MSDN, могут его пропустить.

Итак, нам потребуются:

  1. Visual Studio 2010 в любой редакции.
  2. Silverlight 5 Tools for Visual Studio 2010 SP1 (http://www.microsoft.com/download/en/details.aspx?id=28358). Для тех, кто по каким-либо причинам не хочет или не может установить себе Silverlight 5 и Silverlight 5 Tools, доступен также отдельный дистрибутив WCF RIA Services: http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=28357. Сборки, находящиеся в нем, будут работать как в проектах под Silverlight 5, так и в проектах под Silverlight 4, и при этом не обязательно иметь установленный пакет обновлений SP1 для Visual Studio 2010.
  3. WCF RIA Services Toolkit (September 2011): http://www.microsoft.com/download/en/details.aspx?id=26939. Это дополнительный набор сборок, которые необязательны для работы самого WCF RIA Services, но потребуются нам, в частности, для перегрузки работы кодогенератора.

Наше пробное Silverlight-приложение будет отображать записи из таблицы БД на сервере, позволять их редактировать и отправлять изменения на сервер.

1. Создадим новый проект типа Silverlight Application и назовем его RIATest. В диалоге New Silverlight Application выберем версию Silverlight 5 (или хотя бы Silverlight 4) не забудем выставить галочку Enable WCF RIA Services.


2. Добавим в серверный проект RIATest.Web новую SQL Server Database и назовем ее RIATestDB (чтобы база создалась успешно и к ней можно было присоединиться прямо из Visual Studio, на машине должен быть установлен и запущен экземпляр SQL Server с именем .\SQLExpress).

3. Добавим в БД RIATestDB новую таблицу Vehicles с ключевым полем ID типа uniqueidentifier и строковыми полями Manufacturer и Model. Добавим в таблицу парочку записей.

4. Создадим новый контекст данных LINQ2SQL (LINQ to SQL Classes), назовем его RIATestModel и перетащим на него таблицу Vehicles. Теперь в нашем серверном проекте появилась объектная модель данных, которую мы хотим сделать доступной в клиентском Silverlight-приложении.

5. Соберем все решение (если этого не сделать, то только что созданная нами модель данных RIATestModel не появится в мастере создания сервиса доступа к данным на следующем шаге).

6. Создадим новый сервис доступа к данным. Сделаем это с помощью меню Project->Add New Item… В открывшемся диалоге в разделе Installed Templates->Web найдем пункт Domain Service Class, введем имя RIATestDomainService.cs и нажмем кнопку Add.


7. На экране отобразится диалог Add New Domain Service Class. Проставим там галочки в соответствии с рисунком:


и нажмем ОК. На этом этапе в нашем серверном проекте RIATest.Web появился класс RIATestDomainService, унаследованный от LinqToSqlDomainService<TContext> - базового класса для сервисов доступа к данным, использующим LINQ2SQL-контексты. Причем, в класс RIATestDomainService автоматически добавились все необходимые методы для доступа к таблице Vehicles.

8. Соберем все решение. Теперь, если в окне Solution Explorer выбрать клиентский проект RIATest и нажать на кнопку «Show All Files» на панели инструментов, то мы увидим, что в проект добавился автосгенерированный файл RIATest.Web.g.cs. Этот файл обновляется кодогенератором WCF RIA Services каждый раз при изменении сервиса доступа к данным в серверном проекте и содержит клиентские классы (будем называть их классами-суррогатами) для сервиса и всех сущностей, доступ к которым он предоставляет.


9 Теперь откроем форму MainPage.xaml в редакторе и выберем меню Data->Show Data Sources. Клиентский автосгенерированный контекст RIATestDomainContext уже окажется там, и все, что нам останется сделать – это перетащить сущность Vehicle на форму. При этом на форме окажется источник данных для нашего контекста и DataGrid, отображающая его содержимое.


10. Если теперь запустить проект RIATest.Web, мы увидим данные из таблицы Vehicles в окне Silverlight-приложения. Но для ровного счета и для полноты картины добавим на форму кнопку Save и в обработчике нажатия на нее напишем единственную строчку кода:

      this.vehicleDomainDataSource.SubmitChanges();

Написав всего одну эту строчку, мы получили то, на что раньше (без WCF RIA Services) потратили бы не меньше пары сотен:


RAD-программирование мышкой не перестает производить впечатление со времен Delphi 3 и по сей день. Но, к сожалению, описанная выше процедура (большинство статей на тему WCF RIA Services в сети ею и ограничиваются) подходит только для сферического проекта в вакууме и в реальной жизни может пригодиться очень редко. Потому что в реальной жизни при разработке корпоративных бизнес-приложений нам, скорее всего, придется не строить, а надстраивать. Модель предметной области наверняка уже будет как-то реализована, либо ее придется реализовывать путем собирания данных по крупицам из кучи других систем – через Web-сервисы, доступ к чужим БД, XML-файлы и т.п. Иными словами, после титанического объединения множества «ежей» и «ужей» у нас сформируется набор сущностей в виде т.н. POCO-объектов (Plain Old CLR Object) и неких методов доступа к ним. И вот уже этот набор сущностей мы захотим сделать доступными на стороне Silverlight.

К счастью, WCF RIA Services вовсе не привязан к контекстам LINQ2SQL или Entity Framework и полностью поддерживает сценарий работы с POCO-объектами. Дальше мы будем рассматривать именно такой, приближенный к реальности, сценарий, и это поможет нам лучше понять, как там все внутри устроено.

Как это устроено

Для лучшего понимания процесса работы WCF RIA Services давайте сначала точнее опишем суть задачи, которую решает эта технология. Итак, у нас есть модель предметной области в виде набора объектов-сущностей и неких методов, выполняющих над ними CRUD-операции. Сущности могут образовывать связи типа «один-к-одному» и «один-ко-многим», а еще могут обладать нетривиальной логикой инициализации (например, получение значений по умолчанию из каких-нибудь внешних сервисов). Но все это богатство есть у нас на сервере, а мы хотим, чтобы коллекции сущностей автоматически оказались доступны в клиентском Silverlight-приложении, причем, не только для просмотра, но и для изменения. И еще хотим, чтобы с клиента на сервер отправлялись только действительно изменившиеся данные, и чтобы синхронизация изменений происходила не в любой момент времени, а тогда, когда мы скажем (например, по нажатию на кнопку «Сохранить»). Иными словами, мы хотим иметь на клиенте аналог контекста LINQ2SQL или Entity Framework, который сам умеет синхронизироваться с сервером. Собственно, если в двух словах – это и есть основное предназначение WCF RIA Services.

Фреймворк можно условно разбить на 4 основные части:

  1. Набор серверных базовых классов – сборка System.ServiceModel.DomainServices.Server.dll.
  2. Кодогенератор.
  3. Набор базовых классов для сущностей и контекстов на клиентской стороне.
  4. Специализированная реализация WCF-сервиса – сборка System.ServiceModel.DomainServices.Hosting.dll.

1. Базовые классы для сервисов доступа к данным

Как следует из названия сборки System.ServiceModel.DomainServices.Server.dll, она подключается к серверным проектам и содержит базовые классы для реализации сервисов доступа к данным (Domain Services), прежде всего самый важный класс – System.ServiceModel.DomainServices.Server.DomainService. Работая с POCO-объектами, мы будем наследовать наши сервисы доступа к данным именно от него, а для быстрого создания сервисов поверх контекстов LINQ2SQL, Entity Framework и Windows Azure Table Storage серверная сборка предоставляет соответствующих готовых наследников этого класса: LinqToSqlDomainService<T>, DbDomainService<T> и TableDomainService<T>. С LinqToSqlDomainService<T> мы уже познакомились выше, когда создавали пробный проект с помощью мастера.

Помимо наследования от System.ServiceModel.DomainServices.Server.DomainService все сервисы доступа к данным должны быть помечены атрибутом EnableClientAccessAttribute. Более подробно об этом атрибуте мы поговорим в разделе, посвященном безопасности, а пока будем просто знать, что этот атрибут нужен.

Еще один класс в серверной сборке, с которым нам, возможно, придется столкнуться при реализации CRUD-методов – класс System.ServiceModel.DomainServices.Server.ChangeSet. Свойство ChangeSet этого типа предоставляется базовым классом DomainService, оно оказывается заполненным на момент вызова любого create-, update- или delete-метода сервиса доступа к данным и содержит исчерпывающую информацию о том, какие конкретно изменения произошли на клиенте. Содержимое ChangeSet может выглядеть примерно так:


Массив ChangeSet.ChangeSetEntries содержит все сущности, которые были добавлены, изменены или удалены. Если изменяемая сущность является родительской и ссылается на дочерние сущности с применением атрибута [Composition] (подробнее об этом атрибуте см. ниже), этот массив будет содержать также все изменившиеся дочерние сущности, с указанием операции, которая с ними была произведена. Кроме того, через свойство ChangeSet.ChangeSetEntries[i].OriginalEntity можно получить исходное состояние изменившейся сущности. Все эти сведения могут быть полезны при реализации различных нетривиальных алгоритмов сохранения данных.

2. Кодогенератор

Кодогенератор в WCF RIA Services – это то, что делает доступными в клиентском Silverlight-проекте сущности, описанные и реализованные в серверном проекте. Его работа заключается в формировании в клиентском проекте классов-двойников для каждого сервиса доступа к данным и каждой сущности. Но выполняет он эту работу не слишком механически, и даже вносит в процесс некоторую степень интеллектуальности.

ПРИМЕЧАНИЕ

«Интеллектуальность» генератора клиентского кода – это одно из основных архитектурных отличий WCF RIA Services от аналогичных сторонних решений, которые появились задолго до решения от Microsoft (например, библиотеки CSLA.Net). Предшественники WCF RIA Services, как правило, использовали более простой подход: через разделяемые CS-файлы и объявление классов сущностей с ключевым словом partial. Сущность объявлялась в исходном файле, который подключался одновременно к серверному и клиентскому проектам, а специфичная для сервера и клиента функциональность выносилась в отдельные файлы. Основной минус такого подхода заключается в том, что классы модели данных обязательно придется либо писать заново, либо как минимум модифицировать (т.е. не получится взять уже готовую модель данных от другой системы и выставить ее в Интернет). И потом, куча файлов с одинаковыми названиями в разных проектах очень затрудняет навигацию по исходникам и отладку (проверено на практике: Resharper сходит с ума, да и вообще неудобно). Поэтому ребята из Microsoft сознательно не пошли по этому пути.

Сигналом к действию для кодогенератора является выпадающий список WCF RIA Services link в свойствах клиентского Silverlight-проекта (для удобства назовем эту настройку RIA-ссылкой).


Если в этом поле выбран какой-нибудь серверный проект, кодогенератор активируется при каждой компиляции и анализирует этот проект, а также все сборки, на которые он ссылается в поисках сервисов доступа к данным. Для всех найденных сервисов, а также для всех используемых ими сущностей кодогенератор формирует код клиентских классов-суррогатов и записывает его целиком в файл с названием вида <Имя Серверного Проекта>.g.cs в папке Generated_Code Silverlight-проекта. Кроме того, кодогенератор ищет во всем дереве серверных проектов файлы с расширением .shared.cs и без изменений копирует их в ту же клиентскую папку Generated_Code. Механизм shared-файлов позволяет легко разделять между серверными и клиентскими проектами некую общую функциональность, например, статические классы с методами-расширениями (extension methods).

ПРИМЕЧАНИЕ

Каждый Silverlight-проект может иметь RIA-ссылку только на один серверный проект, но этот серверный проект может ссылаться на другие проекты, со своими сервисами доступа к данным. Если наш Silverlight-проект должен использовать сервисы из нескольких серверных проектов, у нас есть два варианта:

1. Создать серверный проект, ссылающийся на все нужные проекты с сервисами, и в Silverlight-проекте сделать RIA-ссылку на него.

2. Для каждого серверного проекта с сервисами создать отдельные Silverlight-проекты (скорее всего, это будут проекты типа Silverlight Class Library) и ссылаться на них из основного Silverlight-приложения.

Важно следить, чтобы в результате таких манипуляций не образовывалось перекрестных ссылок между проектами. Это чревато появлением в клиентском коде автосгенерированных классов с одинаковыми именами и, следовательно, к ошибкам компиляции.

В текущей версии WCF RIA Services 1.0 кодогенератор реализован в виде объектной модели классов в сборке Microsoft.ServiceModel.DomainServices.Tools.TextTemplate.dll. В будущих версиях разработчики обещают полностью перейти на T4-шаблоны (вопрос о том, почему их не стали использовать сразу, является несколько запутанным). Но и сейчас генерацию кода клиентских контекстов и сущностей можно легко настраивать, чем мы и займемся чуть позже на примере тестового проекта.

3. Базовые классы для сущностей и контекстов на клиенте

Откроем теперь любой автосгенерированный файл с расширением .g.cs из папки Generated_Code клиентского проекта и изучим его внутреннее строение.

Базовым классом для всех клиентских контекстов кодогенератор по умолчанию назначает System.ServiceModel.DomainServices.Client.DomainContext. Этот базовый класс обладает функциональностью асинхронной загрузки коллекций сущностей (несколько разновидностей метода Load()), сохранения изменений (два варианта метода SubmitChanges()), а также выполнения invoke-операций (метод InvokeOperation()). Коллекции сущностей для каждого серверного load-метода добавляются в виде свойств в код контекстов, при этом используется специальная реализация коллекции - System.ServiceModel.DomainServices.Client.EntitySet<T>. Помимо этого, все коллекции сущностей контекста становятся доступны через свойство DomainContext.EntityContainer.EntitySets – это очень удобно, если нужно произвести какое-нибудь действие (например, подключиться к событию) со всеми коллекциями сущностей сразу.

Специфика класса коллекции EntitySet<T> (в качестве параметра шаблона должен выступать класс сущности) заключается в умении отслеживать изменения в своих элементах и откатывать эти изменения – для этого класс поддерживает интерфейс IRevertibleChangeTracking. В остальном это обычная коллекция, причем, даже непотокобезопасная (это и не нужно, поскольку, как мы знаем, в Silverlight обработка результатов асинхронных вызовов производится в UI-потоке).

Клиентские инкарнации серверных классов сущностей наследуются от базового класса System.ServiceModel.DomainServices.Client.Entity, который также поддерживает интерфейс IRevertibleChangeTracking для отслеживания и отката изменений. Важно понимать, что наследники класса Entity являются наименьшей неделимой частицей в алгоритме отправки изменений на сервер, например, если в какой-либо сущности с сотней свойств на клиенте изменилось одно-единственное свойство – вся сущность поплывет на сервер целиком. Это очевидный аргумент против запихивания всех данных в одну огромную сущность при проектировании модели предметной области. Кроме того, базовый класс Entity реализует интерфейс INotifyDataErrorInfo, т.е. поддерживает механизм асинхронной валидации.

Урезанной версией класса Entity является класс System.ServiceModel.DomainServices.Client.ComplexObject. От него наследуются все автосгенерированные объекты, не являющиеся сущностями, т.е. такие, у которых нет свойств с атрибутом [Key]. Понятие «комплексных объектов» (так мы в дальшейшем будем называть эти «недо-сущности») используется в моделях LINQ2SQL и Entity Framework для группировки свойств сущностей, чтобы с ними было удобнее работать. Поэтому и WCF RIA Services вынужден их поддерживать. Также от ComplexObject наследуются все классы, которые передаются в качестве параметров и/или возвращаемых значений у invoke-операций. Invoke-операциями мы будем называть все публичные методы сервиса доступа к данным, которые не являются CRUD-методами. Такие методы без изменений транслируются в динамически генерируемый WCF-контракт (об этом контракте будет сказано в следующем разделе) и становятся доступными на клиенте как обычные методы web-сервиса.

ПРИМЕЧАНИЕ

Не вполне приятной фичей нынешней версии кодогенератора, которую скорее следовало бы назвать багом, является то, что все методы сервиса доступа к данным, которые не удается однозначно идентифицировать как CRUD-методы, он пытается втихоря превратить в invoke-операции, а все непримитивные параметры таких методов объявляет комплексными объектами. Чтобы стать invoke-операцией, методу достаточно просто не соответствовать соглашению об именовании. Это сильно мешает людям, которые только начинают знакомиться с WCF RIA Services, понять разницу в парадигмах. Человек вроде бы все правильно написал, нужные классы стали доступны в клиентском коде, и методы для работы с ними тоже появились, все компилируется и даже работает, но работает при этом не так, как предполагали создатели WCF RIA Services: фактически контекст используется как обычный web-сервис, и все преимущества CRUD-модели теряются.

4. Специализированная реализация WCF-сервиса

Организация самого процесса обмена данными в WCF RIA Services покрыта самым густым мраком. Документации в MSDN практически нет, и информацию приходится извлекать, в основном, из блогов разработчиков. Ситуация осложняется тем, что большинство постов на эту тему в Сети относится к предыдущим версиям WCF RIA Services, где все было устроено по-другому. Впрочем, кое-что полезное узнать можно.

Если посмотреть с помощью какого-нибудь профайлера на HTTP-запросы, которые отправляются клиентским приложением на сервер, то можно подсмотреть URL, к которому происходит обращение (на примере запроса от нашего тестового проекта):

http://localhost:18520/ClientBin/RIABlog-Web-RIABlogService.svc/binary/GetBlogs

Как видим, с сервера запрашивается файл объявления WCF-сервиса RIABlog-Web-RIABlogService.svc, которого там физически не существует. Этот файл генерируется «на лету», в момент обработки запроса, примерно в таком виде:

<%@ ServiceHost 
  Service="RIABlog.Web.RIABlogService" 
  Factory="System.ServiceModel.DomainServices.Hosting.DomainServiceHostFactory, System.ServiceModel.DomainServices.Hosting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" 
%>

Здесь параметр Service определяет полное имя класса сервиса доступа к данным (обратите внимание, что оно полностью соответствует имени SVC-файла, только точки заменены дефисами), а параметр Factory задает фабрику для него (System.ServiceModel.DomainServices.Hosting.DomainServiceHostFactory – это штатная фабрика классов сервисов доступа к данным WCF RIA Services).

Генерацией динамического SVC-файла занимается специальный HTTP-модуль - System.ServiceModel.DomainServices.Hosting.DomainServiceHttpModule. Мастер создания серверного проекта WCF RIA Services добавляет код регистрации этого модуля в web.config:

  <system.web>
    <httpModules>
      <!--регистрация Http-модуля WCF RIA Services для IIS 6.0 и IIS 7.0 в режиме Classic -->
      <add name="DomainServiceModule" type="System.ServiceModel.DomainServices.Hosting.DomainServiceHttpModule, System.ServiceModel.DomainServices.Hosting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    </httpModules>
  </system.web>

  …

  <system.webServer>
    <!--регистрация Http-модуля WCF RIA Services для IIS 7.0 в режиме Integrated -->
    <modules runAllManagedModulesForAllRequests="true">
      <add name="DomainServiceModule" preCondition="managedHandler" type="System.ServiceModel.DomainServices.Hosting.DomainServiceHttpModule, System.ServiceModel.DomainServices.Hosting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    </modules>
  </system.webServer>

Задача HTTP-модуля – перехватить запрос к сервисам доступа к данным, найти в сборке серверного проекта соответствующий класс-наследник DomainService (для нашего примера запроса это будет класс с полным именем RIABlog.Web.RIABlogService) и перенаправить ему вызов. Но что значит – перенаправить вызов? Ведь клиент, как видно из URL запроса, пытается вызвать метод WCF-сервиса, а сервисы доступа к данным – это, фактически, обычные классы, они даже не помечены атрибутом [ServiceContract].

Как мы знаем, класс ServiceHost – это входная точка, «слушатель» для обычных WCF-сервисов. Когда мы хотим разместить WCF-сервис, например, в консольном приложении, мы обычно создаем экземпляр ServiceHost, конфигурируем его (указываем ему контракт сервиса, набор привязок (bindings) и поведения (behaviors)) и запускаем ожидание вызовов. Но WCF RIA Services, а конкретно модуль DomainServiceHttpModule, вместо обычного ServiceHost использует его специального наследника - System.ServiceModel.DomainServices.Hosting.DomainServiceHost, который и занимается магией превращения классов сервисов доступа к данным в WCF-сервисы. Используя рефлексию, он генерирует контракт сервиса, а также программно конфигурирует для него привязку. В текущей версии WCF RIA Services по умолчанию подключается только одна привязка – т.н. binary HTTP binding, т.е. объекты сериализуются в бинарный вид и передаются по HTTP. Этот вариант был выбран как наиболее эффективный для CRUD-модели взаимодействия клиента с сервером. Суффикс "-binary" в URL запроса как раз свидетельствует о том, что будет использоваться этот вариант привязки.

Результаты всей этой скрытой деятельности можно увидеть, вставив точку останова в любой метод любого сервиса доступа к данным и изучив содержимое текущего WCF-контекста, который доступен через статическую переменную OperationContext.Current:


Здесь хорошо виден динамически сгенерированный контракт сервиса с отдельными get-методами для каждого типа и общим методом SubmitChanges() для сохранения изменений. Остальные методы контракта (в данном случае это единственный метод ExecuteCommand()) полностью копируют invoke-операции сервиса.

ПРИМЕЧАНИЕ

Возникает вопрос: зачем понадобился такой мудреный механизм с динамически генерируемым SVC-файлом и специальным Http-модулем, перехватывающим клиентские запросы? Объективный ответ на этот вопрос есть. Специальный Http-модуль, помимо всего прочего, умеет разыскивать сервисы доступа к данным во всех папках серверного проекта и предоставлять к ним доступ по унифицированному URL сервера с портом>/ClientBin/<полное имя класса сервиса с дефисами вместо точек>.svc. Т.е. в какую бы серверную подпапку мы ни положили CS-файл с кодом сервиса, его URL будет одним и тем же. А динамический SVC-файл нужен для того, чтобы его можно было подменить обычным статическим файлом с таким же именем. Это, в свою очередь, позволяет нам подставить вместо штатной фабрики сервисов доступа к данным System.ServiceModel.DomainServices.Hosting.DomainServiceHostFactory нашу собственную реализацию фабрики и таким образом вмешаться в процесс инициализации WCF-сервиса. Но эту тему мы, пожалуй, все-таки оставим за рамками данной статьи, хотя в тестовом проекте есть пример того, как это делается.

Отдельно стоит остановиться на вопросах времени жизни и поддержки сессий в сервисах доступа к данным. По умолчанию в коллекцию Behaviours объекта DomainServiceHost добавляется объект ServiceBehaviour со свойством InstanceContextMode, выставленным в PerCall. Как мы знаем из теории создания WCF-сервисов, это означает, что на каждый клиентский запрос будет создаваться отдельный экземпляр класса сервиса. Поэтому не стоит добавлять в класс сервиса доступа к данным нестатические поля и надеяться, что они сохранят свои значения между вызовами. Логика всех методов в сервисах доступа к данным должна быть полностью stateless. При этом нам ничто не мешает использовать в коде этих методов сессии ASP.Net (объект HttpSessionState, доступный через свойство HttpContext.Current.Session) и статические переменные (которые, конечно, должны быть потокобезопасными).

Немного кода

Давайте, наконец, перейдем к детальному изучению кода к статье и на его примере разберем много важных моментов, вроде реализации связи «один-ко-многим», наследования у сущностей и перегрузки кодогенератора.

Модель данных приложения RIABlog

В тестовом проекте реализован простой клиент для ведения собственного блога и просмотра чужих. Незарегистрированный пользователь может только просматривать существующие записи и комментарии к ним. А если зарегистрироваться, то можно создать свой блог (правда, только один), помещать в него записи, а также комментировать записи других блоггеров.

Приложение RIABlog создано на основе шаблона Silverlight Business Application, который устанавливается вместе с WCF RIA Services. Этот шаблон автоматически формирует два проекта – клиентский и серверный, добавляет в оба проекта все необходимые ссылки, связывает их между собой через параметр WCF RIA Services Link (RIA-ссылку), а главное – подключает инфраструктуру для регистрации и аутентификации пользователей, которая по умолчанию использует внутри себя механизмы ASP.Net Membership. Этой инфраструктуре аутентификации, а также тому, как можно вмешаться в ее работу, ниже будет посвящена целая отдельная глава, а пока мы просто порадуемся, что все реализовано за нас и не нужно в сотый раз рисовать форму логина.


Вот так устроена модель данных нашего блог-клиента:


У нас есть сущности блога Blog, записи в блоге BlogPost, а также сущность комментария к записи Comment и объект Keyword для представления ключевых слов. Отдельную ветвь иерархии образуют базовый абстрактный класс команды BaseCommand и три его наследника, которые демонстрируют возможности WCF RIA Services по работе с иерархиями объектов (эти возможности мы рассмотрим позже).

Все сущности идентифицируются глобально-уникальным идентификатором, объявление которого вынесено в базовый абстрактный класс BaseEntity. Причем свойство BaseEntity.Key помечено атрибутом [Key]:

[Key]
[Display(AutoGenerateField = false)]
public Guid Id { get; set; }

Чтобы кодогенератор WCF RIA Services воспринимал класс как сущность, этот класс обязательно должен иметь свойство с атрибутом [Key]. Если у класса такого свойства нет, кодогенератор не считает его сущностью и не генерирует для него соответствующую коллекцию EntitySet в клиентском коде. В нашем примере класс Keyword сущностью не является и поэтому превращается на клиенте в комплексный объект (наследник класса ComplexObject).

Как мы договорились, в нашем примере сущности являются простыми POCO-объектами, поэтому логику загрузки и сохранения для них пришлось реализовать самостоятельно, в соответствующих CRUD-методах сервиса доступа к данным RIABlogService.

ПРИМЕЧАНИЕ

Хранение данных сознательно реализовано максимально просто: в виде иерархии папок и файлов на диске, в папке Blogs серверного проекта. Чтобы еще меньше акцентировать на этой логике внимание, она вынесена в статический класс EntityExtensions в виде набора методов расширения.

Наше клиентское приложение должно прежде всего иметь возможность отобразить список существующих блогов. Get-метод для сущностей Blog объявлен в сервисе RIABlogService так:

        /// <summary>
        /// Выдает список всех блогов
        /// </summary>
        public IEnumerable<Blog> GetBlogs()
{
  …
}

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

        public EntitySet<RIABlog.Web.Models.Blog> Blogs
{
  get
  {
    ...
  }
}

Get-метод в сервисе доступа необязательно должен возвращать IEnumerable<T> (это может быть любой класс, поддерживающий IEnumerable<T>, например, List<T>), но с точки зрения удобочитаемости кода лучше писать именно так. Помимо IEnumerable<T> get-методы могут возвращать IQueryable<T> – именно так происходит в сервисах доступа к данным, которые наследуются от LinqToSqlDomainService<T> или DbDomainService<T>. А еще можно было бы написать get-метод, который возвращает одну сущность:

        public Blog GetMyBlog()
{
  ...
}

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

Аналогичным образом – по сигнатуре и первым буквам имени – кодогенератор отыскивает в сервисе доступа к данным и остальные CRUD-методы для сущности:

        /// <summary>
        /// Создает новый блог. Доступен только аутентифицированным пользователям.
        /// </summary>
[RequiresAuthentication]
publicvoid CreateBlog(Blog newBlog)
{
  …
}

/// <summary>/// Обновляет существующий блог. Доступен только аутентифицированным /// пользователям./// </summary>
[RequiresAuthentication]
publicvoid UpdateBlog(Blog updatedBlog)
{
  …
}

/// <summary>/// Удаляет блог. Доступен только аутентифицированным пользователям./// </summary>
[RequiresAuthentication]
publicvoid DeleteBlog(Blog removedBlog)
{
  …
}
ПРИМЕЧАНИЕ

Для тех, кому по каким-либо причинам тесны рамки соглашения об именовании CRUD-методов, кодогенератор предоставляет альтернативный вариант: пометить методы соответствующими атрибутами [Insert], [Query], [Update] и [Delete].

Обратите внимание, что из всех CRUD-методов обязательным для кодогенератора является только get-метод, остальные можно и не реализовывать, если они нам не нужны. Например, если в нашем сервисе не окажется метода UpdateBlog() или если кодогенератор не сможет определить нужный метод по сигнатуре и имени, то на клиенте все будет работать точно так же вплоть до попытки отправить измененный объект Blog с клиента на сервер с помощью SubmitChanges() – в этот момент случится исключение DomainOperationException.

Реализация связей «один-ко-многим»

Как и в большинстве случаев реальной жизни, в нашем тестовом проекте сущности имеют связи между собой. WCF RIA Services позволяет реализовать такие связи «один-ко-многим» сразу несколькими способами, важно только выбрать наиболее подходящий из них в каждой конкретной ситуации.

Вот у объекта BlogPost есть массив связанных сущностей Comment. Поскольку в нашем тестовом приложении комментарии к записи отображаются на форме вместе с самой записью, нам, конечно, хотелось бы получать этот массив на клиенте вместе с родительской сущностью. Но чтобы это произошло, недостаточно просто объявить в классе BlogPost свойство типа IEnumerable<Comment> (кодогенератор просто проигнорирует такое свойство). Нужно еще разметить его специальными атрибутами:

[Include] 
[Association("Comments", "Id", "PostId")]
public IEnumerable<Comment> Comments { get; set; }

Здесь [Include] – атрибут, который говорит кодогенератору создавать свойство Comments в клиентском классе BlogPost, а также генерировать клиентскую сущность Comment. При этом для объявления свойства Comments кодогенератор будет использовать специальную разновидность коллекции EntityCollection<T>:

        public EntityCollection<RIABlog.Web.Models.Comment> Comments

Атрибут [Association] из пространства имен System.ComponentModel.DataAnnotations играет роль внешнего ключа: указывает кодогенератору и среде исполнения WCF RIA Services, какие свойства связывают сущности BlogPost и Comment. Первый параметр атрибута – это имя ключа (WCF RIA Services его игнорирует, так что писать туда можно что угодно), второй параметр содержит имя ключевого свойства сущности BlogPost, а третий – имя связанного свойства в сущности Comment.

Если таким образом объявить свойство Comments и заполнить его на сервере коллекцией сущностей Comment, сущность BlogPost перекочует на клиента вместе с этой коллекцией. Но что будет, если мы станем изменять содержимое этой коллекции на клиенте, например, добавим туда новый комментарий? В данном случае среда выполнения WCF RIA Services будет искать в сервисе доступа к данным нужные CRUD-методы уже для сущности Comment. В нашем примере в сервисе доступа к данным есть метод CreateComment():

        /// <summary>
        /// Создает новый комментарий. Доступен только аутентифицированным пользователям.
        /// </summary>
[RequiresAuthentication]
publicvoid CreateComment(Comment newComment)
{
  …
}

Он вызывается каждый раз, когда в клиентском приложении мы добавляем комментарий к записи и сохраняем изменения с помощью SubmitChanges().

В некоторых случаях раздельное сохранение родительской сущности и коллекции ее дочерних элементов может оказаться неприемлемым. Например, если это сохранение должно являться атомарной операцией. Тогда можно воспользоваться другим режимом. Если добавить к объявлению свойства Comments атрибут [Composition], то любые изменения коллекции на клиенте (добавление и удаление сущностей в коллекцию, а также изменение сущностей в коллекции) будут приводить к вызову метода UpdateBlogPost(), а приходящий в него экземпляр BlogPost будет содержать всю коллекцию комментариев, в том числе тех, которые не менялись.

Следующий способ представления связи «один-ко-многим» основан на использовании комплексных объектов. Этим способом в тестовом проекте вместе с записями передаются их ключевые слова. Объект Keyword на сервере объявлен как обычный класс:

        public
        class Keyword
{
  publicstring Title { get; set; }
  publicint Popularity { get; set; }
}

Свойство Keywords в сущности BlogPost объявлено как обычный массив объектов Keyword, без специальных атрибутов:

        public IEnumerable<Keyword> Keywords { get; set; }

Тем не менее, кодогенератор генерирует клиентский код для объекта Keyword, только наследует его от ComplexObject. А свойство Keywords объявляет как обычный массив:

        public System.Collections.Generic.IEnumerable<RIABlog.Web.Models.Keyword> 
Keywords

Содержимое массива Keywords благополучно курсирует вместе с родительским объектом BlogPost с сервера на клиента и обратно. Но у такого способа есть как минимум два минуса. Во-первых, если бы массив Keywords содержал тысячи элементов, гонять его каждый раз туда-сюда, очевидно, было бы неэффективно. А во-вторых, поскольку в клиентском коде свойство Keywords оказывается типа IEnumerable<Keywords> (фактически оно содержит объект типа Keywords[]), контекст никак не может отследить изменения свойств элементов этого массива. Т.е. если мы на клиенте захотим изменить значение свойства Popularity у одного-единственного ключевого слова, нам придется создавать новый массив целиком и присваивать его свойству Keywords.

И наконец, осталось разобраться, как быть, если коллекцию дочерних сущностей нужно загружать отдельно от родительской, по мере необходимости. К сожалению, режим ленивой загрузки дается WCF RIA Services в его нынешней версии хуже всего. Скажем, нет возможности сказать среде исполнения загрузить с сервера только объект BlogPost, а его коллекцию комментариев Comments загрузить потом, попозже. Выполнить ленивую загрузку можно только с помощью специального load-метода в сервисе доступа к данным, принимающего в качестве параметра идентификатор родителя и возвращающего коллекцию дочерних сущностей. Именно так реализовано получение записей блога в нашем тестовом приложении:

        /// <summary>
        /// Выдает записи данного блога
        /// </summary>
        public IEnumerable<BlogPost> GetBlogPosts(Guid blogId)
{
  …
}

При щелчке на одном из блогов на домашней странице приложения происходит переход на страницу, отображающую записи выбранного блога. Эта страница в момент своей инициализации запрашивает записи блога с сервера, т.е. вызывает серверный метод GetBlogPosts():

        // очищаем EntitySet от старых записей
        this.BlogContext.BlogPosts.Clear();

// получаем с сервера записи текущего блогаthis.BlogContext.Load(this.BlogContext.GetBlogPostsQuery(this._currentBlogId));

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

Работа с иерархией сущностей

Как быть, если у нас есть несколько наследников некоей базовой сущности, и мы хотим передавать на клиента коллекцию экземпляров таких наследников?

WCF RIA Services поддерживает и такое поведение, причем вполне безболезненно. В нашем тестовом приложении этот механизм показан на примере сущностей команд (пример выглядит несколько избыточно, но его задача – продемонстрировать принцип). В модели есть базовый класс команды BaseCommand и три его наследника, которые по-разному реализуют выполнение команды.

Вот код серверного метода получения коллекции команд:

        /// <summary>
        /// Выдает список команд
        /// </summary>
        public IEnumerable<BaseCommand> GetCommands()
{
  yieldreturnnew AgreeCommand();
  yieldreturnnew DisagreeCommand();
  yieldreturnnew ModerateCommand();
}

В клиентском контексте для такого метода возникнет соответствующая коллекция:

        public EntitySet<RIABlog.Web.Models.BaseCommand> BaseCommands

И нужно только не забыть упомянуть всех его наследников с атрибутом [KnownType] в объявлении базового класса на сервере:

        /// <summary>
        /// Базовый класс для команд
        /// </summary>
[KnownType(typeof(AgreeCommand))]
[KnownType(typeof(DisagreeCommand))]
[KnownType(typeof(ModerateCommand))]
publicabstractclass BaseCommand : BaseEntity
{
  …
}

Атрибуты [KnownType] подскажут кодогенератору, для каких наследников BaseCommand нужно генерировать классы в клиентском коде. Они также нужны, чтобы наследники правильно сериализовались и десериализовались при передаче между сервером и клиентом.

Теперь если на клиенте выполнить загрузку коллекции команд:

        this.BlogContext.Load( this.BlogContext.GetCommandsQuery() );

то в коллекцию BaseCommands попадут объекты нужного типа. И можно, например, отображать такую коллекцию в ItemsControl и менять внешний вид команды в зависимости от ее типа с помощью компонента DataTemplateSelector, который есть в Prism для Silverlight.

Выполнение команды реализовано в виде invoke-операции. Для этого в сервисе доступа к данным есть метод, помеченный атрибутом [Invoke]:

        /// <summary>
        /// Выполняет команду
        /// </summary>
        /// <param name="command"></param>
        /// <param name="entityId"></param>
        /// <returns></returns>
[Invoke]
publicstring ExecuteCommand(BaseCommand command, Guid entityId)
{
  // передаем выполнение самому объекту командыreturn command.Execute(entityId);
}

Как мы выяснили выше, invoke-операции можно и не помечать никакими атрибутами, но в данном случае атрибут [Invoke] необходим, потому что в качестве параметра передается класс сущности, и кодогенератору нужна подсказка, как правильно себя вести.

Работа с клиентским контекстом

В течение всей жизни приложения RIABlog создается один общий, используемый всеми страницами приложения экземпляр контекста RIABlogContext. Это происходит в обработчике события Application.OnStartup:

        // Создаем общий экземпляр контекста для всего приложения
        this.BlogContext = new RIABlogContext();

// Делаем его доступным для связыванияthis.Resources.Add("BlogContext", this.BlogContext);

Сразу после создания экземпляр контекста становится доступным через публичное свойство BlogContext объекта Application, а также добавляется в коллекцию статических ресурсов приложения, чтобы сделать возможным привязку к его коллекциям сущностей из любого места XAML-кода:

<sdk:DataGrid 
  ItemsSource="{Binding Path=Blogs, Source={StaticResource BlogContext}}" 
>

Таким образом, все данные на клиенте у нас хранятся в одном месте, и каждая сущность гарантированно окажется в единственном экземпляре. Однако теперь нам придется прилагать дополнительные усилия для поддержания общего экземпляра контекста в консистентном состоянии при навигации между страницами. Ведь если пользователь, к примеру, отредактировал название своего блога, а потом нажал на кнопку «Назад» в браузере, его изменения должны либо отправиться на сервер, либо быть полностью отменены до перехода на другую страницу! Для обеспечения этого все страницы в проекте унаследованы от базового абстрактного класса BlogPageBase, в котором переопределен виртуальный метод OnNavigatingFrom():

        /// <summary>
        /// Запрещаем переход на другую страницу без сохранения изменений
        /// </summary>
        /// <param name="e"></param>
        protected
        override
        void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
  // если были изменения, выполняем сохранениеif (this.BlogContext.HasChanges)
  {
    e.Cancel = true;

    if 
    (
      MessageBox.Show
      (
        ApplicationStrings.SaveChangesQueryText,
        ApplicationStrings.QuestionText, 
        MessageBoxButton.OKCancel
      ) 
      == 
      MessageBoxResult.OK
    )
    {
      this.SaveChanges(e.Uri);
    }

    return;
  }

  base.OnNavigatingFrom(e);
}

При каждой попытке перехода между страницами выполняется проверка на наличие не отправленных на сервер изменений, и если таковые есть, переход отменяется. При этом, с согласия пользователя запускается процесс сохранения изменений, и только в случае его успешного завершения происходит принудительный переход на запрошенную пользователем страницу. Для этого используется внутренний метод SaveChanges(Uri uriNavigateTo):

        /// <summary>
        /// Сохранить изменения и перейти
        /// </summary>
        /// <param name="uriNavigateTo">Страница, куда перейти</param>
        protected
        void SaveChanges(Uri uriNavigateTo)
{
  this.BlogContext.SubmitChanges
  (
    op =>
    {
      if (!op.HasError) 
        this.NavigationService.Navigate(uriNavigateTo);
    },
    null
  );
} 
ПРИМЕЧАНИЕ

На самом деле, на этапе проектирования стоит хорошо задуматься над тем, сколько должно быть разных клиентских контекстов (и, соответственно, сколько всего должно быть сервисов доступа к данным на сервере) и сколько экземпляров каждого контекста создавать на клиенте. Подход, используемый в тестовом проекте, вряд ли будет применим для большого бизнес-приложения с сотнями сущностей. Общие принципы при выборе оптимального варианта распределения модели данных по контекстам должны быть примерно такими же, как при использовании типизированных датасетов ADO.Net, контекстов LINQ2SQL или Entity Framework (например, можно отталкиваться от понятия сценариев использования (use cases)). Универсального же решения, конечно, не существует.

Перегрузка кодогенератора

Прежде всего, зачем нам вообще может понадобиться менять работу кодогенератора?

Например, мы можем захотеть вынести некую общую функциональность для всех клиентских контекстов в базовый класс, и тогда нам нужно будет уговорить кодогенератор наследовать контексты от этого нового базового класса, а не от штатного DomainContext. Опять же вопрос: зачем? Раз классы контекстов объявляются как partial, мы всегда можем добавить им функциональности в отдельном файле! Но это не всегда бывает удобно, ведь в большом приложении сервисов доступа к данным и, соответственно, клиентских контекстов может быть много, так что копипэйстить один и тот же partial-код в каждый вновь добавляемый контекст – это не лучший метод.

Еще чаще у нас может возникнуть желание сделать базовый класс для клиентских сущностей. Их, понятное дело, будет еще больше, и тут уже метод добавления общей функциональности через partial-классы совсем не работает.

Кодогенератор в WCF RIA Services обладает объектной моделью, которую в урезанном виде (для языка C#) можно изобразить так:


Основной класс модели – абстрактный ClientCodeGenerator, от которого наследуется реализация для C# CSharpClientCodeGenerator. У класса CSharpClientCodeGenerator есть пять виртуальных свойств, которые должны возвращать реализации генераторов конкретных разновидностей клиентских объектов. Например, свойство DomainContextGenerator возвращает генератор клиентских контекстов, а EntityGenerator – очевидно, выдает генератор сущностей.

Таким образом, изменить поведение кодогенератора очень просто – нужно создать наследника CSharpClientCodeGenerator, пометить его специальным атрибутом [DomainServiceClientCodeGenerator] и переопределить в нем свойства так, чтобы они возвращали также перегруженные генераторы клиентских классов. Но главная хорошая новость в том, что нам не нужно этого наследника никак регистрировать или куда-то прописывать – достаточно просто реализовать его в серверном проекте. В коде нашего тестового приложения наследник CSharpClientCodeGenerator реализован так:

[DomainServiceClientCodeGenerator("RIABlogClientCodeGenerator", "C#")]
publicclass RIABlogClientCodeGenerator : CSharpClientCodeGenerator
{
  /// <summary>/// Подменяем генератор клиентских контекстов/// </summary>protectedoverride DomainContextGenerator DomainContextGenerator
  {
    get { returnnew RIABlogDomainContextGenerator(); }
  }

  /// <summary>/// Подменяем генератор клиентских сущностей/// </summary>protectedoverride EntityGenerator EntityGenerator
  {
    get
    {
      returnnew RIABlogEntityGenerator();
    }
  }
}

WCF RIA Services сам найдет этого наследника в скомпилированной сборке серверного проекта и станет использовать его вместо своего штатного CSharpClientCodeGenerator.

ПРИМЕЧАНИЕ

Поиск и загрузка наследников CSharpClientCodeGenerator происходит с помощью IoC-контейнера MEF (Managed Extensions Framework). Это предопределяет главную проблему, на которую можно напороться, играясь с кодогенератором. В поисках этих наследников MEF сканирует папку bin серверного проекта, загружает оттуда все сборки, какие найдет, а также весь граф сборок, на которые те ссылаются, вплоть до System.dll. Если хоть одну сборку из этого графа не удается найти и/или загрузить (например, сборка осталась прописана в качестве ссылки, но уже нигде не используется и физически отсутствует на диске), MEF кидает исключение – и WCF RIA Services просто тихо переключается на штатный кодогенератор. Это, понятно, ведет к невнятным ошибкам компиляции клиентского проекта, либо просто к потере какой-либо нужной функциональности. А отследить, какая же именно сборка вызвала проблему, можно только по содержимому окна Output во время компиляции, причем для этого придется увеличить уровень детализации вывода (Tools->Options->Projects and Solutions->Build and Run->MSBuild project build output verbosity).

Как видно из кода, в тестовом проекте перегруженный класс RIABlogClientCodeGenerator через свои свойства возвращает также перегруженные варианты генератора контекстов RIABlogDomainContextGenerator и генератора сущностей RIABlogEntityGenerator, в которых переопределен единственный виртуальный метод TransformText(). Все это нужно только для того, чтобы в клиентском коде подменить базовые классы для контекстов и сущностей на RIABlogBaseDomainContext и RIABlogBaseEntity, соответственно. Вот код RIABlogDomainContextGenerator:

        public
        class RIABlogDomainContextGenerator : CSharpDomainContextGenerator
{
  publicoverridestring TransformText()
  {
    // подставляем базовый класс для клиентских контекстовreturnbase.TransformText().Replace
    (
      "System.ServiceModel.DomainServices.Client.DomainContext",
      "RIABlogBaseDomainContext"
    );
  }
}

В клиентском проекте базовый класс контекстов RIABlogBaseDomainContext (он, разумеется, наследуется от DomainContext) переопределяет все четыре метода обращения к серверу: Load(), SubmitChanges() и две разновидности InvokeOperation(). В них добавлено глобальное отключение функциональности окна приложения на время выполнения асинхронных операций и показ сообщений об ошибках (при этом вызывающий код не потерял возможности добавлять собственную обработку ошибок через callback-методы, если ему это нужно). Также полезным может оказаться дополнительная реализация метода Load(), которая умеет параллельно запускать несколько асинхронных операций загрузки и вызывать callback-метод по завершении их всех:

        public
        void Load(EntityQuery[] queries, Action<LoadOperation[]> callback)
{
  // задизабливаем интерфейсthis.App.SetIsBusy(ApplicationStrings.LoadingDataText);

  var results = new List<LoadOperation>();

  // этот счетчик будет захвачен лямбдой int queryCount = queries.Length;

  foreach(var query in queries)
  {
    base.Load
    (
      query, 
      LoadBehavior.KeepCurrent, 
      op =>
      {  
        // Код в этой лямбде будет выполняться в UI-потоке.// Поэтому блоки не нужны и фокус с захватом локальной переменной// вполне безопасен.// Инициализируем все сущности, включая дочерние.this.InitEntities(op.AllEntities);

        if (op.HasError)
        {
          op.MarkErrorAsHandled();
        }

        // сохраняем результат выполненной операции
        results.Add(op);

        if((--queryCount) <= 0) // когда все операции выполнятся
        {
          this.App.ClearIsBusy();

          if(callback != null)
          {
            // если есть обработчик от вызывающего кода - используем его
            callback(results.ToArray());
          }
          else
          {
            // иначе выполняем общую обработку ошибокvar operationsWithErrors = results.Where(result => result.HasError);

            if (operationsWithErrors.Count() > 0)
            {
              ErrorWindow.CreateNew
              (
                operationsWithErrors.Aggregate
                (
                  string.Empty, 
                  (s, r) => s + r.Error.Message + "\n"
                )
              );
            }
          }
        }
      }, 
      null
    );
  }
}

В качестве счетчика завершившихся асинхронных операций здесь используется локальная переменная queryCount, которую захватывает тело анонимного метода. Сам этот анонимный метод будет вызван несколько раз (по завершении каждой операции), причем будет вызван в UI-потоке приложения, поэтому в его код не нужно вставлять никакой синхронизации. При каждом вызове захваченная значение переменной уменьшается, а при ее обнулении вызывается общий callback-метод. Использование замыкания здесь позволяет элегантно обойтись без дополнительных классов и глобальных переменных. И кстати, идею можно применять не только при работе с WCF RIA Services, но и для параллельного вызова методов любых web-сервисов в Silverlight.

Вопросы безопасности

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

WCF RIA Services предлагает достаточно простой декларативный подход для управления доступностью данных и предоставляет для этого набор готовых атрибутов. Как мы уже знаем, чтобы сервис доступа к данным стал доступен для обращений клиентов, он должен быть помечен атрибутом [EnableClientAccess]:

[EnableClientAccess]
publicclass RIABlogService : DomainService
{
  …
}

Этот атрибут является последним барьером на пути сервиса в открытый доступ, фактически он нужен для того, чтобы программисту труднее было ошибочно вывести в онлайн сервис, который не должен быть доступным.

Атрибут [RequiresAuthentication] делает методы сервиса недоступными для анонимных пользователей. Его можно применять как ко всему классу сервиса доступа к данным, так и к отдельным методам. Попытка выполнить защищенный этим атрибутом метод до выполнения аутентификации пользователя приведет к исключению на стороне сервера. В нашем тестовом проекте атрибутом [RequiresAuthentication] помечены, например, методы создания, изменения и удаления блога.

Еще один полезный атрибут – [RequiresRole] – позволяет декларативно разграничить доступ к методам сервиса в зависимости от наличия у текущего пользователя определенных ролей. [RequiresRole] также применяется либо к методу, либо ко всему классу сервиса. Роли идентифицируются строками и указываются в качестве параметров атрибута:

[RequiresRole("Administrators", "BlogOwners")]
publicvoid CreateBlog(Blog newBlog)
{
  …
}

Причем если указать в параметрах несколько ролей, то атрибут будет проверять, назначена ли пользователю хотя бы одна роль из перечисленных.

Теперь давайте разберемся, чем с точки зрения сервиса отличается аутентифицированный пользователь от анонимного. Поскольку в недрах WCF RIA Services находится почти обыкновенный WCF-сервис и поскольку работает он под IIS, механизмы аутентификации в нем используются тоже вполне обыкновенные для ASP.Net-приложения.

Самый простой путь для разработчика приложения на WCF RIA Services - использовать Windows-аутентификацию. В этом случае даже в web.config вставлять ничего не надо, т.к. это режим аутентификации по умолчанию для ASP.Net. Нужно только создать проект с помощью шаблона Silverlight Application. Аутентификация производится браузером при отправке первого же HTTP-запроса на сервер, в случае Silverlight-приложения это будет запрос на получение XAP-файла.

Если вставить точку останова в любой метод любого сервиса доступа к данным и запустить проект в режиме отладки, то в статическом свойстве HttpContext.CurrentUser можно увидеть примерно такой объект WindowsPrincipal:


Этот объект и используется инфраструктурой WCF RIA Services для проверки прав доступа текущего пользователя, причем в качестве ролей будет использоваться набор групп, в которые он входит.

Windows-аутентификация обычно хорошо подходит для разработки бизнес-приложений. Но не всегда: бывает, что учетные записи пользователей хранятся не только в Active Directory, но и еще где-нибудь, например, в БД разных внутренних систем, с которыми бизнес-приложение должно интегрироваться. Для приложений, ориентированных на широкие массы интернет-пользователей (таких, как наш тестовый проект), Windows-аутентификация вообще неприменима. Альтернативой в таких случаях является Forms-аутентификация, и WCF RIA Services предоставляет для этого готовую инфраструктуру.

Forms-аутентификация по умолчанию включена в проектах, созданных с помощью шаблона Silverlight Business Application (таких, как наш тестовый проект). Для этого мастер создания проекта Silverlight Business Application делает следующее:

  1. Включает режим Forms-аутентификации в web.config серверного проекта:
<authentication mode="Forms">
<forms name=".RIABlog_ASPXAUTH" timeout="2880" />
</authentication>
  1. Здесь ".RIABlog_ASPXAUTH" – это имя cookie-файла, который будет содержать шифрованные данные сеанса, а 2880 – время жизни этого файла в минутах.
  2. Создает в серверном проекте классы сущностей User (профиль пользователя) и RegistrationData (данные для регистрации нового пользователя).
  3. Создает два специальных сервиса доступа к данным – AuthenticationService и UserRegistrationService. Первый из них реализует аутентификацию и авторизацию пользователя, т.е. умеет проверять логин/пароль и выдавать список ролей. Второй используется в момент регистрации нового пользователя и содержит всего один метод – CreateUser().
  4. Добавляет в клиентский проект готовые формы для логина и для ввода регистрационных данных, а также код инициализации инфраструктуры в файл App.xaml.cs. В частности, туда добавляется код создания экземпляра специального контекста аутентификации WebContext (этот класс тоже генерируется кодогенератором, но генерируется особенным образом и по-разному, в зависимости от того, есть ли в серверном проекте сервис аутентификации или нет):
WebContext webContext = new WebContext();
// используем Forms-аутентификацию:
webContext.Authentication = new FormsAuthentication(); 
this.ApplicationLifetimeObjects.Add(webContext);
  1. И код, который делает попытку аутентифицировать пользователя при старте приложения на основании сохраненного cookie-файла (чтобы работала галочка «Оставаться в системе» в диалоге логина):
WebContext.Current.Authentication.LoadUser(this.Application_UserLoaded, null);

Если теперь посмотреть на содержимое свойства HttpContext.Current.User в момент выполнения любого метода сервиса, то мы увидим там стандартный объект RolePrincipal, который предоставляется ASP.Net. Причем до выполнения аутентификации он будет выглядеть так:


А после аутентификации – так:


Для хранения и получения данных пользователя сервисы AuthenticationService и UserRegistrationService по умолчанию используют штатные механизмы ASP.Net Membership, ASP.Net Roles и ASP.Net Profile. Но поведение этих сервисов легко можно переопределить, поскольку большинство методов базового класса AuthenticationService - виртуальные, а код класса UserRegistrationService вообще полностью генерируется мастером создания проекта. В тестовом проекте, например, в сервисе аутентификации перегружен метод ValidateUser(), туда добавлена проверка логина/пароля в локальном хранилище SAM, чтобы можно было выполнять вход под своей обычной учетной записью Windows:

      protected
      override
      bool ValidateUser(string login, string password)
{
  // сначала ищем пользователя в базе Membershipif(base.ValidateUser(login, password))
  {
    returntrue;
  }

  // потом пытаемся найти его среди локальных пользователей системыusing(var context = new PrincipalContext(ContextType.Machine))
  {
    return context.ValidateCredentials(login, password);
  }
}

Сервис AuthenticationService наследуется от предоставляемого WCF RIA Services обобщенного класса AuthenticationBase<T>, причем в качестве параметра типа базовому классу передается класс профиля пользователя User. AuthenticationBase<T> умеет сохранять и загружать все свойства сущности User в ASP.Net Profile, и это позволяет очень просто расширять профиль пользователя дополнительными данными. Для этого достаточно добавить нужные свойства в класс User:

      public
      partial
      class User : UserBase
{
  /// <summary>/// Настоящее имя пользователя/// </summary>publicstring FriendlyName { get; set; }

  /// <summary>/// Время последнего логина/// </summary>public DateTime LastLoginTime { get; set; }
}

И сконфигурировать раздел ASP.Net Profile в web.config так, чтобы названия и типы данных свойств профиля соответствовали вновь добавленным свойствам класса User:

<profile>
  <properties>
    <add name="FriendlyName"/>
    <add name="LastLoginTime" type="System.DateTime"/>
  </properties>
</profile>
ПРИМЕЧАНИЕ

После редактирования раздела <profile> в web.config нужно не забыть удалить файл базы данных ASP.Net (по умолчанию, это файл App_Data\ASPNETDB.MDF), чтобы он создался заново при следующем обращении. При этом, конечно, текущие данные пользователей потеряются.

Теперь дополнительные свойства профиля становятся доступны на клиенте, их можно изменять и сохранять на сервер с помощью метода WebContext.Current.Authentication.SaveUser():

      // обновляем данные профиля пользователя
WebContext.Current.User.LastLoginTime = DateTime.Now;
WebContext.Current.Authentication.SaveUser(false);

Инфраструктура WCF RIA Services предоставляет практически все, что нужно рядовому разработчику рядового бизнес-приложения для реализации проверки прав доступа. Единственное, чего не хватает в шаблоне Silverlight Business Application – это готовой формы для администрирования пользователей, т.е. назначения им ролей. Там есть только пример программного задания роли пользователю средствами ASP.Net Roles в момент регистрации:

      // В случае проблем с менеджером ролей, лучше если сбой будет сейчас, чем после создания пользователя.
      if (!Roles.RoleExists(UserRegistrationService.DefaultRole))
{
  Roles.CreateRole(UserRegistrationService.DefaultRole);
}

...

// Назначить пользователю роль по умолчанию.// Это не удастся, если управление ролями отключено.
Roles.AddUserToRole(user.UserName, UserRegistrationService.DefaultRole);

Так что в случае использования Forms-аутентификации немного потрудиться над средством управления правами пользователей все-таки придется.

Последнее, что нужно помнить про Forms-аутентификацию вообще и Forms-аутентификацию в WCF RIA Services в частности – это то, что Forms-аутентификация сама по себе небезопасна. Логин и пароль передаются с клиента на сервер в теле HTTP-пакета в незащищенном виде, поэтому благоразумие подсказывает нам реализовывать хотя бы этот этап с применением шифрованного протокола HTTPS. Применительно к WCF RIA Services это означает, что сервисы аутентификации и регистрации пользователей всегда должны быть доступны только через шифрованную точку подключения. Добиться этого можно простым переключением протокола для серверного web-приложения с HTTP на HTTPS в настройках IIS (в отладочных целях можно переключить протокол на вкладке Debug в свойствах проекта), при этом все сервисы доступа к данным автоматически переключатся на безопасный протокол. Но можно и забыть это сделать при развертывании приложения на релизной среде. Чтобы это не привело к фатальным последствиям, разработчики WCF RIA Services предусмотрели параметр RequiresSecureEndpoint у атрибута [EnableClientAccess] и рекомендуют всегда выставлять его в true для сервисов AuthenticationService и UserRegistrationService:

[EnableClientAccess(RequiresSecureEndpoint = true)]
publicclass AuthenticationService : AuthenticationBase<User>
{
  …
}

Помеченный таким атрибутом сервис просто не заработает по небезопасному протоколу. Разумеется, параметр RequiresSecureEndpoint можно выставлять в true и для любых других сервисов, транслирующих между клиентом и сервером важную информацию.

Имеем на выходе

Надо признать, «введение» в технологию WCF RIA Services получилось довольно объемным. А ведь еще осталось нераскрытым множество тем, к примеру, использование сервисов доступа к данным в не-Silverlight-приложениях, валидация данных, создание клиентов под Windows Phone 7, сравнение возможностей WCF RIA Services и параллельно развивающегося фреймворка WCF Data Services, вопросы производительности и масштабирования… Буду рад по возможности помочь ответами на вопросы на форумах RSDN. Ну и конечно, Google нам всем в помощь!

Список литературы

  1. Документация по WCF RIA Services в MSDN - http://msdn.microsoft.com/en-us/library/ee707344(VS.91).aspx.
  2. Блог Брэда Абрамса, теперь уже бывшего product manager'а проекта WCF RIA Services - http://blogs.msdn.com/b/brada/archive/tags/riaservices/.
  3. Блог Кайла Мак-Клилана, разработчика из команды WCF RIA Services - http://blogs.msdn.com/b/kylemc/archive/tags/wcf+ria+services/.
  4. Блог Фредрика Нормена, просто классного специалиста - http://weblogs.asp.net/fredriknormen/archive/tags/WCF+Ria+Services/default.aspx.


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 1    Оценка 105 [+1/-0]         Оценить