Текст
                    Построение СОА с использованием Windows Communication Foundation
Создание служб
О’REILLY®
^ППТЕР*
Джу вел Лёве

Programming WCF Services Juval Lowy O’REILLY” Beijing • Cambridge • Farnham • Koln • Paris • Sebastopol • Taipei • Tokyo
Создание служб Windows Communication Foundation Лжувел Лёве С^ППТЕР Москва • Санкт-Петербург • Нижний Новгород * Воронеж Ростов-на-Дону • Екатеринбург * Самара * Новосибирск Киев • Харьков • Минск 2008
Джувел Лёве Создание служб Windows Communication Foundation Серия «Бестселлеры O’Reilly» Перевели с английского Е. Матвеев и А. Пасечник Заведующий редакцией Руководитель проекта Научные редакторы Корректор Верстка ББК 32.973-018.2 УДК 004.451 А. Сандрыкин П. Маннинен Е. Матвеев, А. Пасечник В. Листова Л. Харитонов Дж. Лёве Л35 Создание служб Windows Communication Foundation. — СПб.: Питер, 2008. — 592 с.: ил. ISBN 978-5-91180-763-4 Книга посвящена новой и, по мнению многих специалистов, революционной объединенной платформе для разработки сервис-ориентированных приложений для Windows. В первой части объясняются все преимущества использования сервис-ориентированной архитектуры, далее подробно, на практических примерах, показано, как для этого использовать Windows Communication Foundation. Особое внимание в книге уделяется различным тонкостям и наиболее трудным аспектам создания СОА. Читатель не только сможет понять, как программировать, используя WCF, но и освоит на практике важнейшие принципы проектирования. Книга основана на опыте работы автора по разработке стратегии WCF и дальнейшего взаимодействия с командой разработки. Издание адресовано разработчикам и архитекторам, желающим не только освоить WCF, но и подняться на ступень выше в проектировании и разработке ПО. Права на издание получены по соглашению с O'Reilly. Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав. Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книги. ISBN 978-0596526993 (англ.) ISBN 978-5-91180-763-4 © O'Reilly, 2007 © Перевод на русский язык ООО «Питер Пресс», 2008 © Издание на русском языке, оформление ООО «Питер Пресс», 2008 ООО «Питер Пресс», 198206, Санкт-Петербург, Петергофское шоссе, д. 73, лит. А29. Налоговая льгота — общероссийский классификатор продукции ОК 005-93, том 2; 95 3005 — литература учебная. Подписано в печать 17.03.08. Формат 70x100/16. Усл. п. л. 56,76. Тираж 2300. Заказ 8564. Отпечатано по технологии CtP в ОАО «Печатный двор» им. А. М. Горького. 197110, Санкт-Петербург, Чкаловский пр., д. 15.
Содержание Предисловие........................ Введение .......................... Как устроена книга.............. Для кого написана книга.......... Что потребуется для работы с книгой . Благодарности.................... Глава 1. Основные сведения о WCF . . Что такое WCF?................... Службы........................... Адреса........................... Контракты........................ Хостинг.......................... Привязки ........................ Программирование на стороне клиента.............. Сравнение административной и программной настройки......... Архитектура WCF.................. Работа с каналами................ Надежность....................... Глава 2. Контракты служб............ Перегрузка операций.............. Наследование контрактов.......... Факторинг и проектирование контрактов служб................ Запросы на поддержку контрактов . . . Глава 3. Контракты данных........... Сериализация ..................... Атрибуты контракта данных......... Иерархия контрактов данных........ Эквивалентность контрактов данных. . Изменение версий.................. Перечисления ..................... Делегаты и контракты данных .... Наборы данных и таблицы........... Обобщенные типы................... Коллекции ........................ . 7 10 11 14 15 15 16 16 17 20 22 26 32 50 58 59 61 65 70 70 72 77 81 88 88 95 106 114 116 124 126 126 131 134 Глава 4. Управление экземплярами . 144 Аспекты поведения................144 Управление экземплярами для служб уровня вызова..........145 Сеансовые службы.................151 Синглетные службы................161 Демаркационные операции..........166 Деактивизация экземпляров........169 Регулирование нагрузки...........174 Глава 5. Операции...................181 Операции «запрос-ответ»..........181 Односторонние операции...........182 Операции обратного вызова........186 События .........................208 Потоковая передача...............212 Глава 6. Сбои и исключения..........216 Контракты сбоев..................219 Расширения обработки ошибок .... 231 Глава 7. Транзакции.................246 Восстановление после сбоев.......246 Транзакции.......................247 Распространение транзакций.......254 Класс Transaction................264 Транзакционное программирование служб............................266 Явное транзакционное программирование.................282 Управление состоянием служб .... 293 Управление экземплярами и транзакции.....................295 Обратный вызов...................315 Глава 8. Управление параллельной обработкой .........................322 Управление экземплярами и параллельная обработка.........323 Режим параллельной обработки службы...........................323 Экземпляры и параллельный доступ . . 331
6 Содержание Ресурсы и службы.....................332 Контекст синхронизации ресурсов. . . 335 Синхронизационный контекст службы . 343 Пользовательский синхронизационный контекст службы....................355 Обратный вызов и безопасность клиента..........................361 Обратные вызовы и синхронизационный контекст . . . 364 Асинхронные вызовы...............372 Глава 9. Очереди....................387 Отключенные службы и клиенты . . . 387 Очереди вызовов..................388 Управление экземплярами..........403 Управление параллельной обработкой . 410 Сбои при доставке................411 Сбои при воспроизведении.........419 Вызовы с очередями и подключенные вызовы............424 Ответная служба......................427 НТТР-мосты...........................446 Глава 10. Безопасность..................453 Аутентификация.......................453 Авторизация..........................454 Безопасность передачи................455 Управление личностью.................462 Общая политика.......................462 Сценарный подход.....................463 Интрасетевые приложения..............464 Интернет-приложения..................493 Приложение «бизнес-бизнес».......515 Отсутствие безопасности..............523 Декларативная инфраструктура безопасности ................... 525 Контроль средств безопасности . . . . 542 Приложение А. Введение в служебно- ориентированное программирование............547 Краткая история программирования . . 547 Объектно-ориентированное программирование.................549 Компонентно-ориентированное программирование.................549 Служебно-ориентированное программирование.................552 Преимущества служебно-ориентированного подхода........................553 Служебно-ориентированные приложения......................554 Каноны и принципы...............555 Практические принципы...........556 Необязательные принципы.........557 Приложение Б. Схема «публикация/подписка» .... 558 Схема «публикация/подписка» .... 559 Инфраструктура «публикация/подписка» ......... 560 Приложение В. Стандарты программирования WCF. . 577 Общие рекомендации из области проектирования ................ 577 Основные положения..............578 Контракты служб.................578 Контракты данных................579 Управление экземплярами.........580 Операции и вызовы...............580 Сбои............................582 Транзакции......................583 Управление параллельной обработкой.....................584 Службы с очередями..............585 Безопасность .................. 586 Алфавитный указатель ...............587
Предисловие Мы с Дж. Лёве, автором этой превосходной книги, связаны общим увлечени- ем - проектированием и построением распределенных систем. При этом мы прошли очень похожий путь в области технологий, хотя всегда работали в раз- ных компаниях над разными проектами, а большую часть карьеры — даже на разных континентах. Вначале 1990-х годов мы оба медленно вживались в идею «сделать что-ни- будь на одном компьютере, чтобы что-то произошло на другом компьютере», а мир технологий прикладных распределенных систем еще только начинал формироваться. Со временем оборудование рабочих станций и серверов стано- вилось более доступным, и построение крупных систем, не зависящих от цен- трального транзакционного узла, стало привлекательным с экономической точ- ки зрения. Прогресс наблюдался и в области пересылки данных. Сейчас в это трудно поверить, но тогда моя телефонная компания настаивала, что передача данных по телефонной линии со скоростью свыше 1200 бит/с никогда не будет возможна. В настоящий момент по тому же проводу идет передача со скоро- стью 6 Мбит/с и более. Интересные были времена. В процессе формирования технологий доступных распределенных вычисле- ний, в начале 1990-х годов появились два основных «лагеря»: технология DCE, во главе которой стояла фирма Digital Equipment Corporation (со временем по- глощенная фирмой Compaq, которая также была поглощена ИР), и технология CORBA, развиваемая консорциумом OMG с изрядной! поддержкой IBM. В 1996-1997 годах работа над этими замечательными проектами практически остановилась. Появился Интернет, и мир начал сходить с ума по (1) HTML. (2) HTTP, (3) венчурным предприятиям, и (4) первичному размещению акций. Чтобы прийти в себя от лопнувшего «Интернет-пузыря», отрасли понадоби- лось почти 10 лет. Проблемы лежа.-!и не только в экономической, но еще и в техно- логической плоскости. С другой стороны, были и положительные последст- вия - вместо двух основных технологий распределенных систем осталась толь- ко одна... пли несколько десятков, в зависимости от точки зрения. В 2007 году отрасль все еще не может прийти к единому мнению относи- тельно «правильного пути» написания кода распределенных систем. Вероятно, представители Sun Microsystems или ВЕА скажут вам, что это Java; мои колле- ги в Microsoft (и я сам) наверняка предпочтут C# или Visual Basic. Но ни в Sun, ни в ВЕА, ни в IBM, ни в Microsoft уже нет разногласий по поводу того, как должно быть организовано взаимодействие на канальному уровне. Войны «DCE против CORBA» остались в прошлом, и сам факт появления согласованной
8 Предисловие спецификации, заложенной в основу этого перемирия — SOAP 1.1, — был весь- ма впечатляющим достижением. Прошло почти шесть лет с тех пор, как спецификация SOAP 1.1 была подана в W3C в виде технической записки. С тех пор было разработано и согласовано множество спецификаций на базе SOAP, от фундаментальных (адресация и широкий спектр средств безопасности) до протоколов уровня предприятия (например, координации атомарных транзакций). Моя рабочая группа в Microsoft, которая до сих пор неформально называет- ся «Indigo» по кодовому названию проекта, занимала центральное место в этих усилиях по разработке и согласованию. Если бы не сильное желание IBM, Microsoft и других партнеров создать общий набор стандартов (при том, что все мы яростно конкурируем в области промышленных технологий), сама система открытых стандартов не могла бы существовать, не говоря уже о ее многочис- ленных реализациях от разных поставщиков для разных платформ. Откровенно говоря, все это заняло на пару лет больше, чем мы рассчитыва- ли. На согласование уходит время, и мы просто не выпускали свой программ- ный продукт — Windows Communication Foundation — без полной уверенности в том, что он будет нормально работать с продуктами наших партнеров и кон- курентов. Проектирование тоже требует времени, и при разработке нового про- дукта мы стремились к тому, чтобы разработка на новой платформе выглядела знакомой для наших клиентов, потративших время и усилия на освоение техно- логий распределенных систем предыдущей волны — служб ASP.NET, WSE (Web Services Enhancements), NET Remoting, Messaging/MSMQ и Enterprise Services/COM+. Только что приведенный список содержит названия пяти технологий, а с учетом их аналогов в неуправляемом коде и вспомогательных ветвей их бу- дет еще больше. Одну из важнейших целей проектирования Windows Commu- nication Foundation можно охарактеризовать одной простой фразой: единая концепция программирования. Независимо от того, создаете ли вы транзакци- онное N-уровневое приложение, сеть из клиентов P2P, сервер поставок RSS или собственное решение Enterprise Services Bus, вам не придется осваивать множество разных технологий, каждая из которых решает часть поставленной задачи. Изучайте и используйте Windows Communication Foundation — единую концепцию программирования. В этой книге подробно рассказано о том, что мы в Microsoft построили в ка- честве фундамента для построения ваших приложений и служб. Материал из- лагается с той точностью, преподавательским мастерством и архитектурной приверженностью, которые принесли автору заслуженную известность во мно- гих странах. Мы, участники группы Connected Framework из Microsoft, очень гордимся тем, что мы сделали. Созданная нами инфраструктура объединяет семейство распределенных технологий, обладает широкими возможностями взаимодейст- вия, способствует применению служебно-ориентированного программирова- ния, не особенно сложна в освоении и чрезвычайно эффективна в работе.
Предисловие Дж. Лёве — один из самых выдающихся экспертов современности в области распределенных систем, и нам лестно, что он посвятил свою книгу Windows Communication Foundation. Надеемся, он поможет вам понять, почему мы все и сообщество пользователей ранних версий продукта с таким энтузиазмом от- носимся к самому продукту и создаваемым им возможностям. Желаю приятно- го чтения и удачи при построении ваших первых служб WCF! Клеменс Вастерс, руководитель проекта, Connected Framework Team Microsoft Corporation
Введение В августе 2001 года я впервые познакомился с проектом Microsoft по перера- ботке СОМ+ с использованием управляемого кода. Довольно долго ничего ин- тересного не происходило. Затем в июле 2002 года, на встрече C# 2.0 Strategic Design Review были представлены общие планы но переработке технологий удаленного доступа в нечто, чем смогут пользоваться разработчики. Тогда же компания Microsoft вела работы по включению новых спецификаций безопас- ности веб-служб в стек ASMX и активно взаимодействовала с другими партне- рами по созданию предварительных версий дополнительных спецификаций веб-служб. В июле 2003 года мне представилась возможность ознакомиться с новой транзакционной инфраструктурой, в которой были исправлены многие недос- татки транзакционного программирования .NET. На тот момент еще не сущест- вовало логически связной программной модели, объединявшей эти разнород- ные технологии. К концу 2003 меня пригласили в немногочисленную группу внешних экспертов для участия в стратегической оценке проекта новой плат- формы разработки с кодовым обозначен нем «Indigo». Некоторые из самых ум- ных и хороших людей, которых я знал, также вошли в эту группу. За последую- щие' 2-3 года проект «Indigo» прошел три поколения программных моделей. Текущая декларативная модель, построенная на базе конечных точек, появи- лась в начале 2005 года, стабилизировалась к августу и в конечном итоге полу- чила название WCF (Windows Communication Foundation). Если спросить нескольких людей, что такое WCF, вы вряд ли получите оди- наковые ответы. Для разработчика веб-служб WCF — решение, которое на- конец-то решает проблему совместимости, реализация длинного списка про- мышленных стандартов. Для разработчика распределенных приложений — это простейший механизм организации удаленных вызовов и даже вызовов с очередями. Для разработчика систем — это следующее поколение средств, ориентированных на производительность (транзакции, хостинг и т.д.), предос- тавляющих готовый вспомогательный, «вторичный» код для приложений. Для прикладного программиста -- это декларативная модель программирования для структурирования приложении. Наконец, для архитектора WCF представ- ляет долгожданную возможность построения служебно-ориентированных при- ложений. В действительности WCF сочетает в себе все перечисленные аспекты, просто потому, что платформа проектировалась таким образом — как следую- щее поколение разных технологий Microsoft. Для меня WCF — всего лишь следующая платформа разработки, которая в значительной степени заменяет низкоуровневое программирование .NET. Предполагается, что WCF будет использоваться всеми разработчиками .NET независимо от типа приложения, его размера или отраслевого сектора. WCF —
Как устроена книга 11 фундаментальная технология, предоставляющая простой и логичный способ построения служб и приложений в соответствии с тем, что на мой взгляд явля- ется разумными архитектурными принципами. Платформа WCF изначально строилась для упрощения разработки и развертывания приложений, а также снижения общих затрат их владельцев. Службы WCF используются для по- строения служебно-ориентированных приложений, от автономных настольных приложений до веб-приложений и служб, а также высокопроизводительных приложений уровня крупного предприятия. Как устроена книга В книге рассматриваются различные темы и навыки, необходимые для проек- тирования и разработки служебно-ориентированных приложений на базе WCF. Читатель увидит, как правильно использовать встроенные средства WCF — хостинг служб, управление экземплярами, управление параллельной обработ- кой, транзакции, отключенные вызовы с очередями и безопасность. Хотя в кни- ге показано, как пользоваться перечисленными средствами, основное внимание уделяется ответам на вопрос «почему» и обоснованиям тех или иных конкрет- ных архитектурных решений. Материал не ограничивается программировани- ем WCF и связанным с ними системными вопросами; читатель также найдет в книге рекомендации по проектированию, советы и предупреждения о возмож- ных ловушках. Практически все темы представлены с точки зрения программи- ста, потому что цель автора — сделать читателя не только экспертом в WCF, но и повысить его профессиональный уровень как программиста. Книга избегает подробностей реализации WCF; изложение материала в ос- новном сконцентрировано на возможностях и практических аспектах примене- ния WCF — применению технологии, выбору архитектуры и программной мо- дели. Материал в полной мере использует возможности .NET 2.0, а в некоторых случаях также требует хорошего знания С#. В книге также представлены многие вспомогательные классы и программы, написанные мной. Эти классы и атрибуты предназначены для повышения эф- фективности труда и качества написанных вамп служб WCF. Я разработал не- большую инфраструктуру, работающую поверх WCF, которая компенсирует некоторые недочеты в архитектуре и упрощает выполнение некоторых задач. Кроме того, моя инфраструктура демонстрирует возможности расширения WCF. За прошедшие два года я опубликовал в MSDN Magazine несколько статей, посвященных WCF. В настоящее время я также веду раздел WCF в рубрике «Foundations». Некоторые из этих статей были взяты за основу для глав книги; я благодарен журналу, который разрешил мне это сделать. Даже если вы уже знакомы с этими статьями, соответствующие главы все равно стоит прочитать. Книга содержит гораздо более полную информацию, включая альтернативные точки зрения, практические приемы и примеры, а ее содержание часто связано с темами других глав.
12 Введение Каждая глава посвящена отдельной теме. Впрочем, материал каждой главы базируется на содержимом предыдущих глав, поэтому читать следует по по- рядку. Далее приводится краткий перечень глав и приложений. Глава 1. Основные сведения о WCF В начале главы приводится общее объяснение, что такое WCF, после чего опи- сываются важнейшие концепции и структурные блоки WCF — адреса, контрак- ты, конечные точки, хостинг и клиенты. Далее следует общее объяснение архи- тектуры WCF, хорошее понимание которой является залогом для понимания всех последующих глав. Предполагается, что читатель понимает основные пре- имущества служебно-ориентированного программирования. Если это не так, начните с приложения А. Даже если вы уже знакомы с основными концепция- ми WCF, я рекомендую хотя бы бегло просмотреть эту главу — не только что- бы убедиться в прочности имеющихся знаний, но и для знакомства с некото- рыми вспомогательными классами и терминами, которые будут использоваться в книге. Глава 2. Контракты служб Глава посвящена проектированию и использованию контрактов служб. Снача- ла разбираются некоторые полезные приемы перегрузки контрактов служб и наследования. Далее речь пойдет о проектировании и факторинге контрактов, хорошо подходящих для повторного использования, удобных в сопровождении и расширении. Напоследок я покажу, как организуется программное взаимо- действие с метаданными контрактов. Глава 3. Контракты данных Основная тема главы — обмен данными между клиентом и службой без переда- чи информации о типе данных или необходимости использования той же тех- нологии разработки. Рассматривается ряд интересных практических вопросов, в том числе контроль версии данных и передача коллекций. Глава 4. Управление экземплярами Глава отвечает на важный вопрос: какой экземпляр службы обрабатывает тот или иной запрос клиента? WCF поддерживает несколько механизмов управле- ния экземплярами, активизацией и управлением жизненным циклом, причем выбор имеет серьезные последствия для масштабируемости и производитель- ности приложения. В главе приводятся обоснования каждого режима управле- ния экземплярами, рекомендации относительно того, когда и как их лучше ис- пользовать, а также рассматриваются некоторые сопутствующие темы — например, регулирование нагрузки.
Как устроена книга 13 Глава 5. Операции В этой главе рассматриваются типы операций служб, вызываемых клиентом. Читатель найдет в ней полезные рекомендации из области проектирования — как улучшать и расширять базовые возможности для поддержки обратного вы- зова, управлять каналами и портами обратного вызова и обеспечить работу ти- пизованных дуплексных посредников. Глава 6. Сбои и исключения Вся глава посвящена одной теме: тому, как службы передают клиентам инфор- мацию о возникающих ошибках и исключениях. Такие конструкции, как ис- ключения и обработка исключений, привязаны к конкретным технологиям и не могут выходить за границы служб. В этой главе рассматриваются рекомендуе- мые методы обработки ошибок, обеспечивающие логическую изоляцию кли- ентской обработки ошибок от службы. Также читатель узнает, как происходит расширение возможностей базового механизма обработки ошибок. Глава 7. Транзакции Сначала объясняется необходимость применения транзакций, после чего обсу- ждаются многие аспекты работы транзакционных служб: архитектура управле- ния транзакциями, конфигурация распространения транзакций, декларативная поддержка транзакций в WCF, а также возможность создания транзакций кли- ентами. В завершение главы рассматриваются сопутствующие вопросы из об- ласти проектирования — такие, как режимы активизации экземпляров и управ- ление состоянием транзакционных служб. Глава 8. Управление параллельной обработкой Данная глава описывает простой, но мощный механизм декларативного управ- ления параллельной обработкой и синхронизацией (как на стороне клиента, так и на стороне службы). Затем рассматриваются более сложные вопросы: об- ратные вызовы, реентерабельность, контексты синхронизации, а также приво- дятся рекомендации по предотвращению взаимных блокировок. Глава 9. Очереди Глава посвящена организации очередей вызовов к службам; очереди делают возможными асинхронную, отключенную работу служб. В начале главы рас- сматривается настройка служб с очередями, после чего обсуждение переходит к таким аспектам, как транзакции, управление экземплярами и сбои, а также их влиянию на бизнес-модель службы и ее реализацию. Глава 10. Безопасность В последней главе многогранная тема безопасности разделяется на простые со- ставляющие (передача сообщений, аутентификация, авторизация и т. д.). Далее
14 Введение будет показано, как обеспечивается безопасность в типовых сценариях (напри- мер, интрасетевых иди интернет-приложениях). В завершение я представлю свою инфраструктуру декларативной безопасности WCF, автоматизирующую настройку системы безопасности и существенно упрощающую работу со сред- ствами безопасности. Приложение А. Введение в служебно-ориентированное программирование Приложение написано для читателей, которые хотят понять, что же собой пред- ставляет служебно-ориентированное программирование. Я предлагаю свое ви- дение служебно-ориентированного программирования и смысл его применения в конкретном контексте. В приложении определяются служебно-ориентирован- ные приложения (в отличие от простой архитектуры) и службы, а также рас- сматриваются преимущества методологии. Тема завершается анализом принци- пов служебно-ориентированного программирования, причем абстрактные каноны дополняются рядом практических аспектов, необходимых для боль- шинства приложений. Приложение Б. Схема «публикация/подписка» В этом приложении описана моя инфраструктура для реализации схемы управ- ления событиями «публикация/подписка», позволяющая создавать службы публикации и подписки буквально в одной-двух строках кода. Хотя данная схе- ма с таким же успехом могла стать частью главы 5, я вынес ее в отдельное при- ложен не. потому что в ней используются аспекты из других глав — такие, как транзакции и вызовы с очередями. Приложение В. Стандарты программирования WCF По сути это объединенный список рекомендаций и замечаний типа «делай так» или «так не делай», приводившихся в книге. Стандарт отвечает на вопросы «как» и «что», а не «почему» - за обоснованиями следует обращаться к другим главам книги. Кроме того, в стандарте используются вспомогательные классы, упоминаемые в книге. Для кого написана книга Предполагается, что читатель является опытным разработчиком, хорошо разби- рающимся в объектно-ориентированных концепциях (таких, как инкапсуляция и наследование). Я использую ваш существующий опыт использования объект- ных и компонентных технологий, а также знание терминологии и перенесу его в WCF. В идеале читатель должен неплохо разбираться в .NET, а также владеть C# хотя бы на базовом уровне (включая параметризованные классы и аноним-
Благодарности 15 ные методы). Хотя в книге в основном используется С#, материал также может быть использован разработчиками Visual Basic 2005. Что потребуется для работы с книгой Для работы с книгой вам потребуется .NET 2.0, Visual Studio 2005, компоненты .NET 3.0, пакет разработки .NET 3.0 SDK и расширения .NET 3.0 для Visual Studio 2005. Если в тексте прямо не упомянуто обратное, материал книги относит- ся к Windows ХР, Windows Server 2003 и Windows Vista. Возможно, также потре- буется установить дополнительные компоненты — такие, как MSMQ и I IS. Благодарности Я познакомился с платформой WCF на ранней стадии ее существования, и это произошло только благодаря постоянной поддержке и общению с руководите- лями проекта WCF (тогда Indigo). Я особенно благодарен своему другу Стиву Сварцу (Steve Swartz), одному из архитекторов WCF, не только за его знания и проницательность, но и за терпение и долгие часы, проведенные за совмест- ным планированием. Благодарю Ясера Шохуда (Yasser Shohoud), Дуга Парди (Doug Purdy) и Шая Коэна (Shy Cohen) за отличные стратегические оценки проекта, а Крита Шринивасана (Krish Srinivasan) — за его почти философский подход к программированию. Работа с ними была лучшим этапом изучения WCF и фактически сама по себе являлась привилегией. Следующие руководи- тели проекта WCF также уделяли мне свое время и помогали разобраться в WCF: Энди Миллиган (Andy Milligan), Брайан Макнамара (Brian McNamara), Юджин Осовецки (Eugene Osovetsky), Кенни Вольф (Kenny Wolf), Кирилл Гаврилюк (Kirill Gavrylyuk), Макс Фейн гольд (Max Feingold), Майкл Маручек (Michael Marucheck), Майк Бернал (Mike Vernal) и Стив Миллет(Steve Millet). Также я благодарен руководителю группы Анджеле Миллс (Angela Mills). Из тех, кто не работал в Microsoft, хочу поблагодарить Нормана Хедлама (Norman Headlam) и Педро Феликса (Pedro Felix) за полезную обратную связь. Спасибо Николасу Пальдино (Nicholas Paldino) за его помощь. Познания Ника в .NET не имеют равных, а его скрупулезное внимание к мелочам оказало ог- ромное влияние на качество и логическую связность материала книги. Наконец, моя жена Дана уговаривала меня записывать свои идеи, отлично зная, что работа над книгой лишает меня драгоценного времени общения с ней и девочками; я также благодарю своих родителей, наделивших меня любовью к программированию. Посвящаю книгу своим дочерям Эбигейл (7 лет) и Эли- нор (4 года). Вы все для меня -- самое главное в жизни.
1 Основные сведения о WCF В этой главе описаны важнейшие концепции и структурные элементы платфор- мы WCF и ее архитектуры. В частности, в ней рассматриваются основные по- нятия, относящиеся к адресам, привязкам, контрактам и конечным точкам; мы разберемся, как организовать хостинг службы и как написать клиента, а также обсудим ряд сопутствующих проблем (внутрипроцессный хостинг и надеж- ность). Даже если читатель уже знаком с основными концепциями WCF, я ре- комендую хотя бы бегло просмотреть эту главу — во-первых, чтобы убедиться в прочном, основательном понимании темы, а во-вторых, потому что некоторые вспомогательные классы и термины, представленные в этой главе, постоянно встречаются и используются в книге. Что такое WCF? Windows Communication Foundation (WCF) — инструментальный пакет (SDK) для разработки и развертывания служб в системе Windows. WCF обеспечивает среду времени выполнения для ваших служб, давая возможность предостав- лять доступ к типам CLR в качестве служб, а также использовать другие служ- бы как типы CLR. Хотя теоретически службы можно создавать и без WCF, на практике WCF значительно упрощает их построение. WCF представляет собой Microsoft-реализацию отраслевых стандартов, определяющих работу со служ- бами, преобразования типов, маршалинг и управление протоколами. Соответ- ственно WCF берет на себя обеспечение взаимодействия служб. В распоряже- ние разработчика предоставляется готовый служебный код, необходимый практически в каждом приложении, поэтому применение WCF кардинально повышает производительность. Первая версия WCF предоставляет много по- лезных инструментов для разработки служб — хостинг, управление экземпля- рами, асинхронные вызовы, надежность, управление транзакциями, очереди ав- тономных вызовов и безопасность. Кроме того, WCF обладает элегантной моделью расширяемости, которая позволяет выйти за рамки базовых возмож- ностей. Кстати говоря, эта модель использовалась при написании самого пакета
Службы 17 WCF. Остальные главы книги посвящены перечисленным аспектам и функци- ям WCF. Практически вся функциональность WCF сосредоточена в одной сборке System.ServiceModel.dll, находящейся в пространстве имен ServiceModel. Платформа WCF является частью .NET 3.0, а для ее работы необходимо присутствие .NET 2.0, поэтому WCF работает только в операционных системах с соответствующей поддержкой. В настоящее время этот список включает Win- dows Vista (клиент и сервер), Windows ХР SP2 и Windows Server 2003 SP1 и более поздних версий. Службы Службой (service) называется функциональный модуль, доступный извне. В этом отношении службы являются очередной ступенью эволюционного пути «функ- ции-объекты-компоненты-службы». Термином SO (Service-orientation) обозна- чается абстрактный набор принципов и оптимальных методов построения прило- жений, ориентированных на работу со службами. Если вы еще не знакомы с основ- ными принципами программирования, ориентированного на работу со службами, обратитесь к приложению А — в нем представлен краткий обзор и побудитель- ные причины для использования SO-программирования. В оставшейся части книги предполагается, что вы уже знакомы с этими принципами. Служеб- но-ориентированные* приложения SOA (Service-Oriented Applications) объ- единяют службы в единую логическую прикладную модель — по аналогии с тем, как компонентно-ориентированные приложения объединяют компонен- ты, или объектно-ориентированные приложения объединяют объекты (рис. 1.1). Рис. 1.1. Служебно-ориентированное приложение Задействованные службы могут быть локальными или удаленными, они мо- гут разрабатываться разными сторонами с применением любых технологий, их 1 Более правильно было бы сказать: «приложения, ориентированные на работу со службами», но то- гда текст станет совсем неудобочитаемым, поэтому далее в книге будет использоваться термин «служебно-ориентированные приложения». — Примеч. перев.
18 Глава 1. Основные сведения о WCF версии могут изменяться независимо друг от друга... даже одновременность их выполнения не является обязательным условием. Внутри служб встречаются всевозможные комбинации языков, технологий, платформ, версий и библиотек, но взаимодействие между службами всегда проходит по четко определенным правилам. Клиентом службы называется сторона, пользующаяся ее функционально- стью. В роли клиента может оказаться буквально все, что угодно — класс Windows Forms, страница ASP.NET, другая служба. Клиенты и службы взаимодействуют друг с другом, отправляя и принимая сообщения. Сообщения могут передаваться от клиента к службе напрямую или через посредника. В WCF все сообщения передаются в формате SOAP. Обрати- те внимание: сообщения независимы от транспортных протоколов — в отличие от веб-служб службы WCF могут взаимодействовать на базе разных транспорт- ных протоколов, не только HTTP. Клиенты WCF могут взаимодействовать со сторонними (не-WCF) службами, а службы WCF могут взаимодействовать со сторонними клиентами. Впрочем, если вы занимаетесь разработкой как клиен- та, так и службы, приложение обычно конструируется так, чтобы на обоих сто- ронах была задействована платформа WCF (для использовании ее преиму- ществ). Так как внутреннее строение службы скрыто от внешнего мира, службы WCF' обычно предоставляют метаданные, описывающие их функциональность и возможные способы взаимодействия со службой. Метаданные публикуются в заранее определенном формате, не зависящем от конкретной технологии — например, WSDL на базе HTTP-GET или отраслевого стандарта обмена мета- данными. Сторонний (не-WCF) клиент может импортировать метаданные в формате родных типов своей среды. Аналогично, клиент WCF может импор- тировать данные сторонних служб и потреблять их в виде интерфейсов и клас- сов CLR. Рис. 1.2. WCF-взаимодействия на одном компьютере
Службы 19 Границы выполнения и взаимодействие со службами В WCF клиент никогда не взаимодействует со службой напрямую, даже при общении с локальными службами в едином пространстве памяти. Вместо этого вызов клиента всегда передается службе через посредника (proxy). Посредник предоставляет те же операции, что и служба, а также ряд управляющих мето- дов. WCF позволяет клиенту взаимодействовать со службой через любые грани- цы выполнения. Если служба и клиент находятся на одном компьютере (рис. 1.2), клиент может обращаться к службе в том же прикладном домене, в другом прикладном домене того же процесса или в другом процессе. Если клиент и служба находятся на разных компьютерах (рис. 1.3), клиент может обращаться к службе в своей интрасети или в Интернете. Рис. 1.3. Межкомпьютерные взаимодействия с использованием WCF WCF и прозрачность размещения1 В прошлом технологии распределенных вычислений — такие, как DCOM и .NET Remoting — стремились предоставить клиенту единую модель программирова- ния для работы как с локальными, так и удаленными объектами. Для локаль- ных обращений клиент использовал прямую ссылку, а работа с удаленным объ- ектом осуществлялась через посредника. Проблема такого подхода, основанного на превращении локальной модели программирования в удаленную, заключа- лась в одном простом обстоятельстве: для удаленного обращения к объекту в действительности требуется нечто большее, чем объект и канал связи. Неиз- бежно возникает целый ряд нетривиальных проблем — управление жизненным циклом, надежность, управление состоянием, масштабируемость и безопас- ность. В результате удаленная модель значительно усложнялась — и все из-за того, что она пыталась выдать себя за то, чем она не была: локальным объектом. WCF также стремится предоставить клиенту единую модель программирова- ния независимо от размещения службы. Тем не менее WCF идет по прямо про- 1 Поскольку Microsoft старается переводить термин location словом «размещение», а не «расположе- ние», то и мы в данной книге последуем ее примеру. — Примеч. перев.
20 Глава 1. Основные сведения о WCF тивоположному пути: берется удаленная модель программирования, основан- ная на работе с экземплярами и посредниками, и применяется даже в самых локальных случаях. Поскольку все взаимодействия осуществляются через по- средника, с едиными требованиями к конфигурации и хостингу, WCF поддер- живает общую модель программирования для локального и удаленного случаев; это не только позволяет менять размещение службы без изменений в клиенте, но и значительно упрощает модель прикладного программирования. Адреса В WCF каждая служба связывается с уникальным адресом. Адрес содержит два важных элемента: размещение службы и транспортный протокол (транспорт - ная схема) для взаимодействия со службой. Первая часть адреса определяет имя целевого компьютера, сайта или сети; коммуникационный порт, канал или очередь; и необязательный конкретный путь, или URL URI (Universal Resource Identifier, универсальный идентификатор ресурса) может содержать любую уникальную строку — например, имя службы или GUID. WCF 1.0 поддерживает следующие транспортные схемы: О HTTP; О TCP; О одноранговая сеть; О IPC (межпроцессные коммуникации по именованным каналам); О MSMQ. Адреса всегда задаются в следующем формате: [базовый адрес]/[необязательный URI] Базовый адрес всегда имеет формат [транспорт]://[компьютер или домен][:необязательный порт] Несколько примеров адресов: http://1ocalhost:8001 http://localhost:8001/MyServlсе net.tcp://1ocalhost:8002/MyServlce net.pi pe://1 oca1 host/MyPIpe net.msmq://1ocalhost/private/MyServi ce net.msmq://localhost/MyService Адрес вида http://1ocalhost:8001 должен читаться так: «Используя HTTP, идем на компьютер localhost, где на порте 8001 кто-то ожидает моего вызова». Если же адрес содержит URI: http://1 оса1 host:8001/MyServlсе его интерпретация будет такой: «Используя HTTP, идем на компьютер loca- lhost, где на порте 8001 кто-то с именем MyService ожидает моего вызова».
Адреса 21 Адреса TCP Адреса TCP содержат транспортный префикс net.tcp. Обычно в них также включается номер порта: net. tcp: / /1 оса 1 host: 8002/MyServ i се Если номер порта не указан, по умолчанию используется порт 808: net. tcp://local host/MyServi ce Два адреса TCP (с одного хоста — см. далее в этой главе) могут совместно использовать один порт: net. tcp://local host :8002/MyServi ce net .tcp: //local host :8002/MyOtherServi ce Адреса TCP часто используются в книге. Адреса HTTP Адреса TCP содержат транспортный префикс http; также встречается префикс https (безопасный транспорт). Адреса HTTP обычно используются с наружны- ми службами на базе Интернета; в них также может указываться порт: http://local host:8001 Если номер порта не указан, по умолчанию используется порт 80. Как и в слу- чае с адресами TCP, два адреса HTTP с одного хоста могут использовать один порт, даже на одном компьютере. Адреса HTTP также часто встречаются в книге. Адреса IPC У адресов IPC транспортный префикс net.pipe указывает на использование меха- низма именованных каналов Windows. В WCF службы, использующие именован- ные каналы, могут принимать вызовы только с того же компьютера. Соответствен- но, адрес должен содержать либо явно заданное имя локального компьютера, либо обозначение localhost, за которым следует уникальная строка с именем канала: net.pipe://] оса Ihost/MyPi ре Именованный канал может быть открыт не более чем в одном экземпляре. Это означает, что два адреса IPC не могут использовать одно имя канала на од- ном компьютере. Адреса IPC тоже встречаются в книге. Адреса MSMQ Транспортный префикс net.msmq в адресах MSMQ указывает на использование механизма очередей Microsoft (Microsoft Message Queue). Имя очереди должно быть указано. При работе с приватными очередями необходимо указать тип очереди, но для публичных очередей тип можно опустить: net.msmq://1 осаIhost/private/MyServiсе net. msmq .//local host/MyServi ce Очереди вызовов рассматриваются в главе 9.
22 Глава 1. Основные сведения о WCF Адреса одноранговых сетей Адреса одноранговых сетей содержат префикс net.p2p, указывающий на ис- пользование транспорта одноранговых сетей Windows. В адресе должно быть задано имя одноранговой сети, а также уникальный путь и порт. Темы конфи- гурации и использования одноранговых сетей выходят за рамки книги, и в по- следующих главах одноранговые сети почти не упоминаются. Контракты В WCF каждая служба предоставляет контракт — стандартный, платформен- но-независимый способ описания того, что делает данная служба. WCF опреде- ляет четыре разновидности контрактов. О Контракты служб описывают операции, которые могут выполняться клиен- том со службой. Они подробно рассматриваются в следующей главе, но ши- роко используются во всех главах книги. О Контракты данных определяют, какие типы данных принимаются и переда- ются службой. WCF определяет косвенные контракты для встроенных типов (таких, как int и string), однако вы можете легко определить явные контрак- ты данных для пользовательских типов. Глава 3 посвящена определению и использованию контрактов данных, а в последующих главах контракты данных используются по мере надобности. О Контракты ошибок определяют, какие ошибки инициируются службой, как служба обрабатывает их и передает своим клиентам. Глава 6 посвящена оп- ределению и использованию контрактов ошибок. О Контракты сообщений позволяют службам напрямую взаимодействовать с сообщениями. Контракты сообщений могут быть типизованными и нети- пизованными; они бывают полезны при решении проблем взаимодействия, а также при наличии существующих форматов сообщений, которые необхо- димо соблюдать. Вам как разработчику WCF почти не придется иметь дела с контрактами сообщений, и в книге они не используются. Контракт службы Атрибут ServiceContractAttribute определяется следующим образом: fAttributellsage(Att ributeTargets. Interface|At.tributeTargets .Class. Inherited = false)1 public sealed class ServiceContractAttribute : Attribute { public string Name {get:set;} public string Namespace {get:set:} // }
Контракты 23 Атрибут используется для определения контракта службы. Он применяется к интерфейсу или к классу, как показано в листинге 1.1. Листинг 1.1. Определение и реализация контракта службы [Servicecontract] interface IMyContract { [Operationcontract] string MyMethod(string text): И He входит в контракт string MyOtherMethod(string text): I class MyService : IMyContract ( public string MyMethod(string text) return "Hello " + text: ) public string MyOtherMethodistring text) return "Cannot call this method over UCF"; Атрибут ServiceContract отображает интерфейс CLR (или обусловленный ин- терфейс, как мы вскоре увидим) на технологически-нейтральный контракт служ- бы. Атрибут ServiceContract представляет интерфейс (или класс) CLR как кон- тракт WCF, независимо от видимости его типа. Видимость типа на WCF вооб- ще не влияет, потому что она относится к концепциям CLR. Применение атрибута ServiceContract к внутреннему интерфейсу раскрывает этот интерфейс как публичный контракт службы, готовый к внешнему использованию. Без ат- рибута ServiceContract интерфейс остается невидимым для клиентов WCF. Все контракты должны быть сформулированы явно: только интерфейсы (и клас- сы), помеченные атрибутом ServiceContract, считаются контрактами WCF. Ос- тальные типы к таковым не относятся. Ни один из членов типа не будет автоматически включен в контракт при ис- пользовании атрибута ServiceContract. Вы должны явно указать WCF, какие ме- тоды включаются в контракт, при помощи атрибута OperationContractAttribute: [Attri buteUsage(Attri buteTargets. Method) ] public sealed class OperationContractAttribute : Attribute public string Name (get:set:} // Атрибут Operationcontract применим только к методам, но не к свойствам, индексаторам или событиям — все они относятся к концепциям CLR. WCF по- нимает только операции (логические функции), а атрибут OperationContract представляет метод контракта как логическую операцию, выполнение которой составляет часть контракта службы. Другие методы интерфейса (или класса),
24 Глава 1. Основные сведения о WCF не имеющие атрибута Operationcontract, в контракт не включаются. Тем самым обеспечиваются принципы четких границ служб и явного формулирования операций. Кроме того, в параметрах контрактных операций не могут использо- ваться ссылки на объекты — разрешены только примитивные типы или кон- тракты данных. Применение атрибута Servicecontract WCF позволяет применять атрибут ServiceContract к интерфейсам и классам. Если атрибут применяется к интерфейсу, некоторый класс должен реализовать этот интерфейс. Обычно для реализации интерфейса используется код C# или VB, и ничто в коде класса службы не указывает на то, что он принадлежит службе WCF: [ServiceContract] Interface IMyContract { [Operationcontract] string MyMethodO; } class MyService : IMyContract { public string MyMethodO { return "Hello WCF"; } } Реализация интерфейса может быть как косвенной, так и явной: class MyService : IMyContract { string IMyContract.MyMethodO { return "Hello WCF"; } } Один класс может поддерживать несколько контрактов; это делается по- средством наследования и реализации нескольких интерфейсов, помеченных атрибутом ServiceContract. [ServiceContract] Interface IMyContract { [Operationcontract] string MyMethodO; } [ServiceContract] Interface IMyOtherContract { [Operationcontract] void MyOtherMethodO; } class MyService : IMyContract.IMyOtherContract
Контракты 25 ( public string MyMethodO (...) public void MyOtherMethodO (...) } Впрочем, для класса реализации службы устанавливается ряд ограничений. Следует избегать параметризованных конструкторов, потому что WCF исполь- зует только конструктор по умолчанию. И хотя класс может использовать внут- ренние свойства, индексаторы и статические члены, клиент WCF ни при каких условиях не сможет работать с ними. WCF также позволяет применить атрибут ServiceContract непосредственно к классу службы, без предварительного определения отдельного контракта: // Неяелательно [ServiceContract] class MyService ( [Operationcontract] string MyMethodO return "Hello WCF": ) } «За кулисами» WCF все равно генерирует определение контракта. Атрибут OperationContract может применяться к любым методам класса — как приват- ным, так и открытым. ВНИМАНИЕ ----------------------------------------------------------------------------------- Постарайтесь обойтись без прямого использования атрибута ServiceContract с классами служб. Все- гда определяйте отдельный контракт, чтобы его можно было использовать в других контекстах. Имена и пространства имен Для своего контракта можно определить пространство имен — более того, это следует сделать. Пространство имен контракта служит в WCF той же цели, что и в программировании .NET: оно определяет область действия типа контракта и снижает вероятность конфликтов. Для определения пространства имен ис- пользуется свойство Namespace атрибута ServiceContract: [Servi ceContract (Namespace = "MyNamespace")] interface IMyContract (•} Если пространство имен контракта не указано, по умолчанию используется значение http://tempuri.org. Для наружных служб обычно используется URL ком- пании, а для интрасетевых служб — любое осмысленное уникальное имя (на- пример, MyApplication). По умолчанию внешнее имя контракта совпадает с именем используемого интерфейса. Однако для контракта можно определить псевдоним, чтобы клиен- ты видели его под другим именем — для этой цели используется свойство Name атрибута ServiceContract:
26 Глава 1. Основные сведения о WCF [ServiceContract(Name="IMyContract")] interface IMyOtherContract Аналогично, внешнее имя публичной операции по умолчанию совпадает с именем метода, но свойство Name атрибута Operationcontract позволяет опреде- лить для него псевдоним-заменитель: [ServiceContract] interface IMyContract { [OperationContractCName = "SomeOperation")] void MyMethod(string text): } Примеры использования этих свойств будут представлены в следующей главе. Хостинг Класс службы WCF не может существовать «сам по себе». Каждая служба WCF должна находиться под управлением некоторого процесса Windows, на- зываемого хостовым процессом. Один хостовой процесс может управлять не- сколькими службами, и один тип службы может иметь несколько хостовых процессов. WCF не требует, чтобы хостовой процесс был (или не был) клиент- ским процессом. Очевидно, наличие отдельного процесса способствует безопас- ности и изоляции ошибок. Также неважно, кто предоставляет процесс или ка- кой тип процесса задействован в хостинге. Хост может предоставляться IIS, службой WAS (Windows Activation Service) в Windows Vista или разработчи- ком как часть приложения. ПРИМЕЧАНИЕ ------------------------------------------------------ Особым случаем является внутрипроцессный хостинг, при котором служба размещается в одном процессе с клиентом. Хост для внутрипроцессной разновидности по определению предоставляется разработчиком. Хостинг IIS Главное преимущество хостинга служб в веб-сервере Microsoft IIS (Internet Information Server) заключается в том, что хостовой процесс автоматически за- пускается по первому запросу клиента, а его жизненный цикл находится под управлением IIS. С другой стороны, главный недостаток хостинга IIS — воз- можность использования только HTTP. В IIS5 устанавливается дополнитель- ное ограничение: все службы должны использовать один номер порта. Хостинг в IIS очень похож на хостинг классических веб-служб ASMX. Вы должны создать в IIS виртуальный каталог и предоставить файл .svc. Файл .svc — аналог файла .asmx — используется для идентификации файла программ- ной логики службы. Синтаксис файла .svc представлен в листинге 1.2.
Листинг 1.2. Название листинга <№ ServiceHost Language - "С#" Debug = "true" CodeBenind = "~/App_Code/MyService.cs" Service -- "MyService" ПРИМЕЧАНИЕ-------------------------------------------------------------------------------- Код службы даже можно встроить в файл .svc, но делать это не рекомендуется, как и в случае с веб- службами ASMX. При использовании хостинга IS базовый адрес службы всегда должен совпа- дать с адресом файла .svc. Visual Studio 2005 Visual Studio 2005 генерирует шаблонный код службы с хостингом IIS. Выпол- ните команду File ► New Website, а затем выберите в окне New Web Site пункт WCF Service. Visual Studio 2005 создает новый веб-сайт, код службы и соответствую- щий файл .svc. Также можно воспользоваться диалоговым окном Add New Item для добавления другой службы. Файл Web.Config В конфигурационном файле сайта (Web.config) должны быть перечислены типы, к которым предоставляется доступ как к службам. В перечислении необ- ходимо использовать полностью уточненные имена типов с включением имен сборок, если тип службы находится в сборке, на которую не существует ссылки: <system.serviceModel> <services> <service name = "MyNamespace.MyServ1ce"> .. J </service> </services> </system.serwiceModel> Автохостинг Термином «автохостинг» обозначается ситуация, при которой разработчик от- вечает за предоставление хостового процесса и управление его жизненным цик- лом. Автохостинг используется как в том случае, если клиент с сервером находят- ся в разных процессах (или на разных компьютерах), так и при внутрипроцессном использовании службы, то есть при ее нахождении в одном процессе с клиентом. Процесс, который должен предоставить разработчик, может быть любым про- цессом Windows — приложением Windows Forms, консольным приложением или службой Windows NT. Учтите, что процесс должен быть запущен до того, как клиент обратится к службе (обычно это означает предварительный запуск). Для служб NT и внутрипроцессного хостинга такой проблемы не существует. 1 Естественно, речь идет не об «автоматическом» хостинге, а о «самохосгинге». - Примеч. перев.
28 Глава 1. Основные сведения о WCF Предоставление хоста обычно реализуется несколькими строками программного кода, и этот способ обладает некоторыми преимуществами перед хостингом IIS. По аналогии с хостингом IIS, в конфигурационном файле хостового прило- жения (Арр.Config) перечисляются типы служб, к которым предоставляется внешний доступ под управлением хоста: <system.servlceModel> <services> <service name = "MyNamespace.MyService"> </service> </services> </system.serviceModel> Кроме того, хостовой процесс должен явно зарегистрировать типы служб во время выполнения и открыть хост для клиентских вызовов; именно по этой причине хостовой процесс должен выполняться до появления клиентских вы- зовов. Как правило, хост создается в методе Main() с использованием класса ServiceHost, как показано в листинге 1.3. Листинг 1.3. Класс ServiceHost public interface ICommunicationObject { void OpenO; void CloseO; // } public abstract class Communicationobject : ICommunicationObject (...) public abstract class ServiceHostBase : CommunicationObject.IDisposable.... {...} public class ServiceHost : ServiceHostBase.... { public ServiceHostCType serviceType.params Uri[] baseAddresses): // При вызове конструктора ServiceHost передается тип службы и возможно (но не обязательно) — базовые адреса по умолчанию. Набор базовых адресов может быть пустым. Даже если базовые адреса передаются, служба может быть на- строена на использование других базовых адресов. Наличие набора базовых ад- ресов позволяет службе принимать вызовы по разным адресам и протоколам, с использованием только относительного URI. Обратите внимание: каждый эк- земпляр ServiceHost связывается с определенным типом службы, и если хосто- вой процесс должен управлять несколькими типами служб, потребуется соот- ветствующее количество экземпляров ServiceHost. Вызов метода Ореп() для хоста разрешает прием вызовов, а вызов Close() обеспечивает корректное завер- шение экземпляра хоста — текущие вызовы будут обработаны, но дальнейшие вызовы от клиентов отклоняются, несмотря на то, что хостовой процесс еще ра- ботает. Закрытие обычно происходит при завершении работы хостового про- цесса. Например, код хостинга следующей службы в приложении Windows Forms:
[ServiceContract] Interface IMyContract (...) class MyService : IMyContract (...) будет выглядеть примерно так: public static void Main() Uri baseAddress = new Uri("http://localhost:8000/"): ServiceHost host = new ServiceHost(typeof(MyService).baseAddress): host.Open0: И Блокирующие вызовы: Application.Run(new MyFormO): host.CloseO: ) Открытие хоста приводит к загрузке runtime-среды ЦСА и запуску рабочих потоков для отслеживания входящих запросов. Благодаря участию рабочих по- токов после открытия хоста можно выполнять блокирующие операции. Явный контроль над открытием и закрытием хоста открывает удобную возможность, ко- торая трудно реализуема при хостинге IIS: вы можете построить специализиро- ванное управляющее мини-приложение, в котором администратор явно открыва- ет и закрывает хост по своему усмотрению, без завершения хостового процесса. Visual Studio 2005 Visual Studio 2005 позволяет включить службу WCF в любой проект приложе- ния; для этого в диалоговом окне Add New Item выбирается пункт WCF Service. Ко- нечно, служба, добавленная подобным образом, является внутрипроцессной по отношению к хостовому процессу, но внепроцессные клиенты тоже могут обра- щаться к ней. Автохостинг и базовые адреса При запуске хоста службы базовые адреса могут вообще не указываться: public static void MainO ServiceHost host = new ServiceHost(typeof(typeof(MyService)): host.OpenO: Application.Run(new MyFormO): host.CloseO; } ВНИМАНИЕ --------------------------------------------------------------------------------- He передавайте null вместо пустого списка, потому что это приведет к возникновению исключения: ServiceHost host: host = new ServiceHost(typeof(typeof(MyService).null):
30 Глава 1. Основные сведения о WCF Также можно зарегистрировать несколько базовых адресов, разделенных за- пятыми, при условии, что адреса не используют одинаковую транспортную схе- му, как в следующем фрагменте (обратите внимание на использование квали- фикатора params в листинге 1.3): Uri tcpBaseAddress = new Url("net.tcp://1 оса 1 host:8001/"); Uri httpBaseAddress = new Uri("http://localhost:8002/"); ServiceHost host = new ServiceHost(typeof(MyService). tcpBaseAddress,httpBaseAddress): WCF также позволяет перечислить базовые адреса в конфигурационном файле хоста: <system.serviceModel> <services> <service name = "MyNamespace.MyService"> <host> <baseAddresses> <add baseAddress = "net.tcp://localhost:8001/"/> <add baseAddress = "http://localhost:8002/"/> </baseAddresses> </host> </service> </services> </system.serviceModel > При создании хоста используется базовый адрес, обнаруженный в файле конфигурации, а также все базовые адреса, заданные на программном уровне. Будьте внимательны и следите за тем, чтобы адреса, заданные в двух разных местах, не пересекались. Допускается даже регистрация нескольких хостов для одного типа службы при условии, что они используют разные базовые адреса: Uri baseAddressl = new Uri("net.tcp://localhost:8001/"): ServiceHost hostl = new ServiceHost(typeof(MyService).baseAddressl): hostl,0pen(); Uri baseAddress2 = new Uri("net.tcp://localhost:8002/"); ServiceHost host2 = new ServiceHost(typeof(MyService),baseAddress2): host2.0pen(); Тем не менее, за исключением некоторых аспектов, связанных с программ- ными потоками (см. главу 8), открытие нескольких хостов подобным образом не дает никаких реальных преимуществ. Кроме того, открытие нескольких хос- тов для одного типа не работает с базовыми адресами, заданными в конфигура- ционном файле, и требует использования конструктора ServiceHost. Расширенные возможности хостинга Интерфейс ICommunicationObject, поддерживаемый классом ServiceHost, предо- ставляет ряд расширенных возможностей. Некоторые из них продемонстриро- ваны в листинге 1.4.
Листинг 1.4. Интерфейс ICommunicationObject public interface ICommunicationObject ( void OpenO. void Close! void Abort(): event EventHandler Closed: event EventHandler Closing: event EventHandler Fdulted; event EventHandler Opened: event EventHandler Opening; lAsyncResult BeginClosetAsyncCal 1 back calIback.object state). lAsyncResult BeginOpen(AsyncCalIback calIback.object state): void EndClose< lAsyncResult result): void EndOpendAsyncResuit result): Communication?taьз State (get;} // public enum CommumcationState ( Created. Opening. Opened. Closing. Closed. Faulted Если операции открытия или закрытия хоста занимают много времени, их можно выполнять асинхронно с использованием методов BeginOpen() и Begin- Close(). Возможна подписка на события хостинга (например, изменения состоя- ния или сбои), а также использование свойства State для проверки состояния хоста. Наконец, класс ServiceHost также реализует метод аварийного заверше- ния Abort(). При вызове Abort() все необработанные вызовы службы немедленно отменяются, а хост завершает свою работу. Активные клиенты получают ис- ключение. Класс ServiceHost<T> Класс ServiceHost, предоставленный WCF, можно усовершенствовать. Для этого мы определим класс ServiceHost<T>, представленный в листинге 1.5. Листинг 1.5. Класс ServiceHost<T> public class ServiceHost<T> : ServiceHost public ServiceHost!) basettypeof(T)) {} public ServiceHosttparams string[] baseAddress) : base(typeof(T).Convert(baseAddresses)) (} продолжение &
32 Глава 1. Основные сведения о WCF public ServiceHost(params Uri[] baseAddress) : base(typeof(T).baseAddresses) {} static Lirin Convert(string[] baseAddresses) Converter<string.llri> convert = delegate(string address) { return new Uri(address): }; return Array.ConvertAl1(baseAddresses.convert): } ServiceHost<T> предоставляет простые конструкторы, которые не требуют пе- редачи типа службы и могут работать с обычными строками вместо неудобных Uri. В книге шаблон ServiceHost<T> будет дополнен целым рядом расширений, функций и возможностей. Хостинг WAS WAS (Windows Activation Service) — системная служба, появившаяся в Win- dows Vista. WAS является частью IIS7, но может устанавливаться и настраи- ваться отдельно. Чтобы использовать WAS для хостинга службы WCF, необхо- димо предоставить файл .svc, как и при хостинге HS. Основное различие между IIS и WAS заключается в том, что WAS не ограничивается протоколом HTTP и может использоваться с любыми доступными в WCF транспортами, портами и очередями. WAS обладает многими преимуществами перед автохостингом: поддержка пулов приложений, повторное использование ресурсов, управление бездейст- вием, управление идентификацией и изоляция. Именно эту разновидность хос- товых процессов следует выбирать там, где это возможно — то есть когда ком- пьютер с Vista Server используется для обеспечения масштабируемости, или клиентский компьютер с Vista выполняет функции сервера для небольшой группы клиентов. Впрочем, у автохостинга тоже имеются свои преимущества — внутрипро- цессный хостинг, решение проблем с неизвестными клиентскими средами и простой программный доступ к расширенным возможностям хостинга, о ко- торых упоминалось ранее. Привязки Взаимодействие с любой конкретной службой имеет много аспектов. К тому же существует немало коммуникационных схем: синхронная передача по принци- пу «запрос/ответ» или асинхронная передача по принципу «отправить и за- быть»; двусторонние сообщения; немедленная доставка или постановка в оче- редь; наконец, сами очереди могут быть надежными или неустойчивыми. Существует много возможных транспортных протоколов для передачи сообще- ний: HTTP (или HTTPS), TCP, P2P (одноранговая сеть), IPC (именованные
Привязки 33 каналы) или MSMQ. Существуют разные варианты кодирования сообщений: пе- редача простого текста для обеспечения совместимости, двоичное шифрование для повышения производительности, МТОМ (Message Transport Optimization Mechanism) для больших объемов полезной информации. Также необходимо учитывать разные варианты безопасности сообщений: отсутствие защиты, за- щита только на транспортном уровне, защита конфиденциальности на уровне сообщений и конечно, разные способы аутентификации и авторизации клиен- тов. Доставка сообщений может быть надежной или ненадежной в отношении промежуточных инстанций и разрывов связи; сообщения могут обрабатываться в порядке их отправки или в порядке получения. Возможно, вашей службе при- дется взаимодействовать с другими службами или клиентами, поддерживаю- щими только простейший протокол веб-служб, или они будут поддерживать со- временные протоколы WS-* (такие, как WS-Security и WS-Atomic Transactions). Нельзя исключать и возможность общения с унаследованными (legacy) клиента- ми посредством низкоуровневых сообщений MSMQ, или же служба будет ограни- чиваться взаимодействием только с другой службой или клиентом WCF. Если начать перечислять все возможные варианты коммуникаций и взаимо- действий, количество всевозможных комбинаций будет исчисляться десятками тысяч. Одни варианты являются взаимоисключающими, другие подразумевают обязательный выбор других вариантов. Конечно, нормальное взаимодействие становится возможным только в том случае, если клиент и служба совмещены по всем допустимым вариантам. Решение сложных проблем такого уровня в большинстве приложений не добавляет практической пользы, но принятие неверных решений будет иметь серьезные последствия для производительно- сти и качества. Чтобы упростить принятие решений и управление наборами вариантов, WCF группирует подобные коммуникационные аспекты в привязках. Привязка (binding) представляет собой логически согласованный, фиксированный набор настроек, относящихся к транспортному протоколу, кодированию сообщений, коммуникационной схеме, надежности, безопасности, распространению тран- закций и совместимости. В идеальном случае все эти «технические» аспекты должны быть выведены из кода службы, чтобы служба могла сосредоточиться исключительно на реализации бизнес-логики. Привязки дают возможность ис- пользовать ту же логику службы со значительно меньшим объемом «техниче- ского» кода. Вы можете использовать привязки WCF в их исходном виде, настраивать их свойства, а также писать собственные привязки «с нуля». Служба публикует свою подборку привязок в метаданных, предоставляя возможность клиентам запрашивать тип и специфические свойства привязки, потому что клиент дол- жен использовать точно такие же параметры привязки, что и служба. Одна служба может поддерживать несколько привязок по разным адресам. Стандартные привязки WCF определяет девять стандартных привязок: О базовая привязка — реализуется классом BasicHttpBinding. Привязка предна- значена для предоставления доступа к службе WCF через унаследованный
34 Глава 1. Основные сведения о WCF механизм ASMX, чтобы старые клиенты могли работать с новыми служба- ми. При использовании на стороне клиента позволяет новым клиентам WCF работать со старыми службами ASMX; О привязка TCP — реализуется классом NetTcpBinding. Привязка использует ТОР для межкомпьютерных взаимодействий в интрасетях. Поддерживает широ- кий набор возможностей, включая надежность, транзакции и безопасность, оптимизирована для взаимодействий WCF-WCF. Как следствие требует, чтобы и клиент, и служба использовали WCF; О привязка одноранговой сети — реализуется классом NetPeerTcpBinding. При- вязка использует одноранговую сеть в качестве транспорта. И клиент, и служ- бы с одноранговой поддержкой подписываются на одну матрицу (grid) и рас- сылают в ней сообщения. Одноранговые сети выходят за рамки книги, потому что для их описания требуется понимание топологии матриц и организации вычислений в сетчатых структурах; О привязка IPC — реализуется классом NetNamedPipeBinding. Привязка исполь- зует именованные каналы в качестве транспорта в пределах компьютера. Данный вид привязок является наиболее защищенным, потому что он не принимает внешние (по отношению к компьютеру) вызовы и поддерживает разнообразные дополнительные функции, как и привязки TCP; О привязка WS (Web Service) — реализуется классом WSHttpBinding. Привязка использует HTTP или HTTPS в качестве транспорта и поддерживает такие возможности, как надежность, транзакции и безопасность в Интернете; О федеративная привязка WS — реализуется классом WSFedrationHttpBindingCLass. Привязка является частным случаем привязок WS с поддержкой федера- тивной безопасности. Тема федеративной безопасности выходит за рамки книги; О дуплексная привязка WS — реализуется классом WSDualHttpBinding. Привязка аналогична привязкам WS, но дополнительно поддерживает двусторонние взаимодействия между службой и клиентом, как обсуждалось в главе 5; О привязка MSMQ - реализуется классом NetMsmqBinding. Привязка исполь- зует MSMQ в качестве транспорта и была спроектирована для поддержки очередей автономных вызовов. Работе с этим видом привязок посвящена глава 9; О интеграционная привязка MSMQ — реализуется классом Msmqlntegration- Binding. Привязка преобразует сообщения WCF в сообщения MSMQ и об- ратно и была спроектирована для взаимодействия с унаследованными кли- ентами MSMQ. Использование данного вида привязок выходит за рамки книги. Формат и кодирование Стандартные привязки используют разные виды транспорта и кодирования, пе- речисленные в табл. 1.1.
Привязки 35 Таблица 1.1. Транспорт и кодирование для стандартных привязок (кодирование по умолчанию выделено жирным шрифтом) Название Транспорт Кодирование Взаимодействие BasicHttpBinding HTTP/HTTPS Текст, MTOM Да NetTcpBinding TCP Двоичное Нет NetPeerTcpBinding P2P Двоичное Нет NetNamedPipeBinding IPC Двоичное Нет WSHttpBinding HTTP/HTTPS Текст, МТОМ Да WSFederationHttpBinding HTTP/HTTPS Текст, МТОМ Да WSDualHttpBinding HTTP Текст, МТОМ Да NetMsmqBinding MSMQ Двоичное Нет MsmqlntegrationBinding MSMQ Двоичное Да Поддержка текстового кодирования позволяет службе (или клиенту) WCF общаться но HTTP с любой другой службой (пли клиентом) независимо от ее технологии. Двоичное кодирование по TCP или IPC обеспечивает наилучшее быстродействие, но за счет ограничения совместимости, так как оно применимо только во взаимодействиях WCF-WCF. Выбор привязки Логика выбора привязки для службы показана на рис. 1.4. Рис. 1-4- Выбор привязки Прежде всего следует спросить себя, должна ли ваша служба взаимодейст- вовать с другими клиентами, кроме клиентов WCF? Если ответ будет положи- тельным, а клиент являе гея унаследованным клиентом MSMQ, выбирайте при- вязку MsmqlntegrationBinding она позволит вашей службе взаимодействовать
36 Глава 1. Основные сведения о WCF с таким клиентом через MSMQ. Если необходимо взаимодействие с не-WCF клиентом, поддерживающим базовый протокол веб-служб (ASMX), выбирайте привязку BasicHttpBinding — служба WCF будет представлена «внешнему миру» так, словно она является службой ASMX. Правда, в этом случае вы лишаетесь возможности использовать большинство современных протоколов WS-*. Если же не-WCF клиент понимает эти стандарты, выбирается одна из привязок WS (WSHttpBinding, WSFederationBinding или WSDualHttpBinding). Если клиент является клиентом WCF, но требует автономного взаимодействия, выбирается привязка NetMsmqBinding, которая позволяет использовать MSMQ для транспорта сооб- щений. Если взаимодействие с клиентом должно проходить только в подклю- ченном состоянии, но клиент и служба находятся на разных компьютерах, вы- бирается привязка NetTcpBinding на базе TCP. Если клиент находится на одном компьютере со службой, выбирается привязка NetNamedPipeBinding, использую- щая именованные каналы для обеспечения максимальной производительности. Выбор уточняется по дополнительным критериям — таким, как необходи- мость использования обратных вызовов (WSDualHttpBinding) или федеративная безопасность (WSFederationBinding). ПРИМЕЧАНИЕ ------------------------------------------------------------------ Многие привязки работают за пределами своих основных сценариев. Например, привязка TCP мо- жет использоваться на одном компьютере и даже во внутрипроцессных комммуникациях, а базовая привязка — в интрасетевых коммуникациях WCF-WCF. И все же старайтесь выбирать привязку в со- ответствии с диаграммой на рис. 1.4. Использование привязок Каждая привязка обладает буквально десятками настраиваемых свойств. Суще- ствуют три режима работы с привязками. Во-первых, вы можете пользоваться встроенными привязками «как есть», если они соответствуют вашим требова- ниям. Во-вторых, можно настроить некоторые из их свойств — таких, как рас- пространение транзакций, надежность и безопасность. В-третьих, вы можете написать собственную пользовательскую привязку. Самый распространенный сценарий — использование существующих привязок почти в исходном виде, с настройкой двух-трех аспектов. Разработчикам приложений вряд ли потребу- ется писать пользовательские привязки, но у разработчиков библиотек такая необходимость может возникнуть. Конечные точки Каждая служба связывается с адресом, определяющим местоположение служ- бы; привязкой, определяющей способ взаимодействия со службой; и контрактом, который указывает, что делает служба. В WCF эта тройка атрибутов формализу- ется в концепции конечной точки. Конечная точка (endpoint) представляет со- бой совокупность адреса, контракта и привязки (рис. 1.5). Каждая конечная точка должна обладать всеми тремя элементами, а хост предоставляет ее внешнему пользователю. На логическом уровне конечная точ- ка определяет интерфейс службы и может рассматриваться как аналог интер- фейса CLR или СОМ. На рис. 1.5 для ее представления используется традици- онная диаграмма типа «леденец на палочке».
ПРИМЕЧАНИЕ------------------------------------------------------------------ На концептуальном уровне, даже в C# и VB, интерфейс является конечной точкой: адрес соответст- вует адресу памяти виртуальной таблицы типа, привязка — JIT-компиляции CLR, а контракт —само- му интерфейсу. В классическом программировании .NET программист не имеет дела с адресами или привязками, а просто принимает их как должное. В WCF адрес и привязка не устанавливаются извне и их приходится настраивать явно. Каждая служба должна предоставлять как минимум одну рабочую конечную точку, и каждая конечная точка имеет ровно один контракт. Все конечные точ- ки службы имеют уникальные адреса, и одна служба может предоставлять не- сколько конечных точек. Эти конечные точки могут использовать одинаковые или разные привязки, а также предоставлять одинаковые или разные контрак- ты. Разные конечные точки, предоставляемые службой, абсолютно никак не связаны друг с другом. Следует помнить, что конечные точки никак не представлены в коде служ- бы; они всегда являются внешними по отношению к коду службы. Конечные точки настраиваются на административном уровне (в конфигурационном фай- ле) или на программном уровне. Административная настройка конечной точки Административная настройка конечной точки осуществляется посредством ее размещения в конфигурационном файле хостового процесса. Для примера возьмем следующее определение службы: namespace MyNamespace { [ServiceContract] interface IMyContract class MyService IMyContract } В листинге 1.6 показаны необходимые записи в конфигурационном файле. Для каждого типа службы перечисляются его конечные точки. Листинг 1.6. Административная настройка конечной точки <system.serviceModel> <services> <service name = "MyNamespace.MyService"> продолжение &
38 Глава 1. Основные сведения о WCF <endpoint address = "http://1ocalhost:8000/MyServiсе/" binding = "wsHttpBinding" contract = "MyNamespace.IMyContract" /> </service> </services> </system.servi ceModel> При указании службы и контракта необходимо использовать полностью уточненные имена типов. Я не буду включать пространства имен в остальных примерах книги, но вы должны использовать их там, где это уместно. Если ко- нечная точка предоставляет базовый адрес, то адресная схема должна соответ- ствовать привязке — например, HTTP с WSHttpBinding. Несоответствие приве- дет к выдаче исключения во время загрузки службы. В листинге 1.7 приведен конфигурационный файл с определением одной службы, предоставляющей несколько конечных точек. Несколько конечных то- чек можно настроить с одним базовым адресом (при условии, что их URI раз- личаются). Листинг 1.7. Определение нескольких конечных точек для одной службы <service name = "MyService"> <endpoi nt address = "http://localhost:8000/MyService/" binding = "wsHttpBinding" contract = "IMyContract" /> <endpoint address = "net.tcp://localhost:8001/MyService/" binding = "netTcpBinding" contract = "IMyContract" /> <endpoi nt address = "net.tcp://localhost:8002/MyService/" binding = "netTcpBinding" contract = "IMyOtherContract" /> </service> В большинстве случаев применяется административная конфигурация, по- тому что она обладает большей гибкостью: адрес службы, привязку и даже кон- тракты можно менять без повторной сборки и развертывания службы. Использование базовых адресов В листинге 1.7 каждая конечная точка предоставляла свой базовый адрес. Явно заданный базовый адрес заменяет любые базовые адреса, которые могли быть предоставлены хостом. Несколько конечных точек могут иметь одинаковый базовый адрес, если ад- реса конечных точек различаются в части URI: <service name = "MyService"> <endpoint address = "net.tcp://localhost:8001/MyService/"
Привязки 39 binding = "netTcpBinding" contract = "IMyContract" /> <endpoint address = "net.tcp.//local host:8001/MyOtherService/" binding = "netTcpBinding" contract = "IMyContract" /> </service> Также возможен другой вариант: если хост предоставляет базовый адрес с под- ходящей транспортной схемой, адрес можно не указывать. В этом случае адрес конечной точки совпадает с базовым адресом транспорта: <endpoint binding = "wsHttpBinding" contract = "IMyContract" /> Если хост не предоставит подходящий базовый адрес, попытка загрузки хос- та завершится с исключением. Задавая адрес конечной точки, можно просто указать относительный URI под базовым адресом: <endpoint address = "SubAddress" binding = "wsHttpBinding" contract = "IMyContract" /> Адрес конечной точки в данном случае складывается из базового адреса и URI (как было сказано ранее, хост обязан предоставить подходящий базовый адрес). Конфигурация привязки Конфигурационный файл может использоваться для настройки привязок, ис- пользуемых конечной точкой. Для этого в секцию endpoint включается тег bindingconfiguration с именем, заданным в секции bindings конфигурационного файла. Назначение тега transaction Flow будет объяснено в главе 7. Листинг 1.8. Конфигурация привязки на стороне службы <system. servi ceMode'! > <services> <service name - "MyService"> <endpoint address = "net.tcp://localhost:8000/MyService/" bindingconfiguration = "TransactionalTCP” binding = "netTcpBinding" contract = "IMyContract" <endpoint address = "net.tcp-//localhost:8001/MyService/" bindingconfiguration = "TransactionalTCP” binding = "netTcpBinding" contract = "IMyOtherContract" продолжение &
40 Глава 1. Основные сведения о WCF </serv1ce> </service> <bindings> <netTcpBinding> <binding name = "TransactionalTCP" transactionFlow = "true" /> </netTcpBinding> </bindings> </system.servi ceModel> Как видно из листинга 1.8, именованную конфигурацию привязки можно использовать в нескольких конечных точках, просто указывая ее имя. Программная настройка конечной точки Программная настройка конечной точки функционально эквивалентна ее адми- нистративной настройке. Вместо использования конфигурационного файла ко- нечные точки включаются в экземпляр ServiceHost программными вызовами. Как и при административной настройке, эти вызовы всегда производятся за пределами кода службы. Класс ServiceHost предоставляет перегруженные вер- сии метода AddServiceEndpoint(): public class ServiceHost : ServiceHostBase { public ServiceEndpoInt AddServiceEndpoint(Type ImplementedContract, Binding binding, string address); // Другие члены } Методам AddServiceEndpoint() могут передаваться как относительные, так и абсолютные адреса, как и при использовании конфигурационных файлов. В листинге 1.9 продемонстрирована программная настройка тех же конечных точек, что и в листинге 1.7. Листинг 1.9. Программная настройка конечных точек на стороне службы ServiceHost host = new ServiceHost(typeof(MyService)): Binding wsBlndlng = new WSHttpBIndlng(): Binding tcpBlndlng = new NetTcpB1nd1ng(); host.AddServiceEndpoi nt(typeofCIMyCont ract).wsBIndlng, "http://1 oca 1 host:8000/MyServlce"): host.AddServiceEndpoi nt(typeof(IMyContract),tcpBInding, "net.tcp://1 oca 1 host:8001/MyServlce"); host.AddServiceEndpoint(typeof(IMyOtherContract).tcpBlndlng, "net.tcp://local host:8002/MyService"): host.OpenO: При добавлении конечной точки на программном уровне адрес задается в формате строки, контракт — в формате Туре, а привязка — в формате одного из субклассов абстрактного класса Binding:
Привязки 41 public class NetTcpBinding • Binding. (...) Если вы намерены использовать базовый адрес хоста, передайте пустую строку (используется только базовый адрес) или только URI для использова- ния комбинации базового адреса с URI): Uri tcpBaseAddress = new Uri ("net .tcp://localhost:8000/"). ServiceHost host = new ServiceHost(tyoeof(MyService). tcpBaseAddress): Binding tcpBinding = new NetTcpBinding(): // Использовать только базовый адрес host.AddServiceEndpoint(typeof(IMyContract) .tcpBinding. ”"); // Использовать относительный адрес host.AddServiceEndpoint(typeof( IMyContract) .tcpBinding. "MyService"): // Игнорировать базовый адрес host.AddServiceEndpoint;typeof( IMyCont ract). tcpBi ndi ng. "net.tcp://1ocalhost:8001/MyServ ice"): host.OpenO: Как и при административной конфигурации, хост должен предоставить под- ходящий базовый адрес; в противном случае происходит исключение. По сути программная настройка почти не отличается от аппаратной. Когда вы исполь- зуете конфигурационный файл, WCF просто разбирает его и выполняет соот- ветствующие программные вызовы. Свойства привязки можно задать на программном уровне. Например, сле- дующий код необходим для включения распространения транзакций по анало- гии с листингом 1.8: ServiceHost host = new ServiceHost(typeof(MyService)): NetTcpBinding tctBinding -- new NetTcpBinding(): tcpBinding.TransactionFlow = true: host.AddServiceEndpoint(typeof(IMyContract). tcpBinding. "net.tcp://local host:8000/MyService"): host.OpenO: Обратите внимание: при настройке свойств привязок обычно используется конкретный субкласс привязки (такой, как NetTcpBinding), а не абстрактный класс Binding, как в примере 1.9. Обмен метаданными Публикация метаданных службы осуществляется двумя способами: по прото- колу HTTP-GET или при помощи специализированной конечной точки (см. далее). WCF может автоматически организовать передачу метаданных по HTTP- GET для вашей службы; все, что для этого потребуется — явно включить соот- ветствующее поведение для службы. Аспекты поведения рассматриваются в следующих главах. А пока достаточно сказать, что аспект поведения является локальной характеристикой службы — например, желает ли она обмениваться
42 Глава 1. Основные сведения о WCF метаданными через HTTP-GET. Поведение включается на административном или программном уровне. В листинге 1.10 приведен конфигурационный файл хостового приложения, в котором обе службы содержат ссылку на секцию, раз- решающую обмен метаданными через HTTP-GET. Адрес, который должен ис- пользоваться клиентами для HTTP-GET, является зарегистрированным базо- вым адресом HTTP для службы. В определении поведения также можно задать внешний URL для этой цели. Рис. 1.6. Страница подтверждения хостинга службы Листинг 1.10. Включение обмена метаданными в конфигурационном файле <system.servi ceModel> <services> <service name = "MyService" behaviorConfguration = "MEXGET"> <host> <baseAddresses> <add baseAddress = "http://localhost:8000/"> </baseAddresses> </host>
Привязки 43 </service> service name = "MyOtherService" behaviorConfguration = "MEXGET"> <host> <baseAddresses> <add baseAddress = "http://localhost:8001/"> </baseAddresses> </host> </service> </services> <behaviors> <serviceBehaviors> <behavior name = "MEXGET"> <serviceMetadata httpGetEnabled = "true”/> </beha vi on </serviceBehaviors> </behaviors> </system. servi ceModel > После включения обмена метаданными по HTTP-GET базовый адрес HTTP (если он имеется) можно открыть в браузере. Если все прошло нор- мально, вы получите страницу подтверждения, показанную на рис. 1.6; это оз- начает, что хостинг службы организован успешно. Страница подтверждения не относится к хостингу I IS; открыть адрес службы в браузере можно даже при ав- тохостинге. Программное включение обмена метаданными Чтобы включить обмен метаданными через HTTP-GET на программном уров- не, необходимо сначала добавить соответствующий аспект поведения в коллек- цию аспектов, поддерживаемую хостом для типа службы. Класс ServiceHostBase содержит свойство Description типа ServiceDescription: public abstract class ServiceHostBase public ServiceDescription Description (get:} // ) Тип ServiceDescription, как следует из его названия, описывает службу со все- ми ее аспектами и характеристиками. ServiceDescription содержит свойство Beha- viors типа KeyedByTypeCollection<I> с обобщенным параметром IServiceBehavior: public class KeyedByTypeCol lection<I> : KeyedCollection<]ype. 1> ( public T F1nd<।>(1: public T RemovnW ); // public clas> ServiceDescription public keyedBylypeCol lection<IServiceBehavior> Behaviors (get.}
44 Глава 1. Основные сведения о WCF Интерфейс IServiceBehavior реализуется всеми классами и атрибутами аспек- тов поведения. KeyedByTypeCoLLection<I> предоставляет шаблонный метод Find<T>(), который возвращает запрашиваемый аспект поведения, если он присутствует в коллекции, или null в противном случае. Конкретный тип аспекта поведения может быть найден в коллекции только один раз. Листинг 1.11 показывает, как включать аспекты поведения на программном уровне. Листинг 1.11. Включение обмена метаданными на программном уровне ServiceHost host = new ServiceHost(typeof(MyServiсе)): ServiceMetadataBehavlor metadataBehavior: metadataBehavior = host.Descri pti on.Behaviors.F i nd<ServiceMetadataBehavior>(); if(metadataBehavior == null) metadataBehavior = new ServiceMetadataBehavior(): metadataBehavior.HttpGetEnabled = true: host.Descript ion.Behaviors.Add(metadataBehavior); } host.OpenO: Сначала код хостинга проверяет, что поведение конечной точки обмена мета- данными не было включено в конфигурационном файле; для этого он вызывает метод Find<T>() класса KeyedByTypeCollection<I> с использованием ServiceMetadata- Behavior в качестве параметра типа. ServiceMetadataBehavlor определяется в про- странстве имен System.ServiceModel.Description: public class ServiceMetaDataBehavior : IServiceBehavior { public bool HttpGetEnabled {get:set.} // } Если возвращаемое значение отлично от null, код хостинга создает новый объект ServiceMetadataBehavlor, задает HttpGetEnabled равным true и включает его в коллекцию аспектов поведения в описании службы. Конечные точки обмена метаданными Служба также может публиковать свои метаданные через специальную конеч- ную точку, называемую конечной точкой обмена метаданными, или сокращенно конечной точкой МЕХ (Metadata EXchange). На рис. 1.7 показана служба с ра- бочими конечными точками и конечной точкой обмена метаданными. Впрочем, конечные точки обмена метаданными обычно не показываются на структурных диаграммах. Конечная точка МЕХ поддерживает отраслевой стандарт обмена метаданны- ми, представленный в WCF интерфейсом IMetadataExchange: [ServiceContract(...)] public interface IMetadataExchange
[Operationcontract(...)] Message Get(Message request): // ... Конечная точка МЕХ Рабочие конечные точки Служба Рис. 1.7. Конечная точка обмена метаданными Подробности интерфейса к делу не относятся — как и большинство отрасле- вых стандартов, он достаточно сложен в реализации. К счастью, WCF может сделать так, что хост службы автоматически предоставит реализацию IMeta- dataExchange и конечную точку обмена метаданными. Для этого необходимо лишь задать адрес и привязку, а также добавить аспект поведения обмена мета- данными. Для привязок WCF предоставляет специализированные транспорт- ные элементы для протоколов HTTP, HTTPS, TCP и IPC. Что касается адреса, вы можете предоставить полный адрес или использовать любой из зарегистриро- ванных базовых адресов. Включать поддержку HTTP-GET при этом не нужно, но вреда от нее тоже не будет. В листинге 1.12 представлена служба, предостав- ляющая три конечные точки МЕХ: для HTTP, TCP и IPC. Для демонстрации конечные точки TCP и IPC используют относительные адреса, а конечная точ- ка HTTP - абсолютный адрес. Листинг 1.12. Добавление конечных точек МЕХ <service name = "MyService" behaviorConfguration = "MEX"> <host> <baseAddresses> <add baseAddress = "net.tcp://localhost:8001/"/> <add baseAddress = "net.pipe://localhost/'7> </baseAddresses> </host> <endpoint> address = "MEX" binding = "mexTcpBinding" contract = "IMetadataExchange" /> <endpoint> address = "MEX" binding = "mexNamedPipeBinding" contract = "IMetadataExchange" /> <endpoint> address = "http://localhost:8000/MEX" binding = "mexHttpBinding" contract = "IMetadataExchange" /> продолжение &
46 Глава 1. Основные сведения о WCF </service> <behaviors> <serviceBehaviors> <behavior name = "MEX"> <serviceMetadata/> </behavior> </serviceBehaviors> </behaviors> Добавление конечных точек МЕХ на программном уровне Конечные точки обмена метаданными, как и любые другие конечные точки, мо- гут добавляться на программном уровне только перед открытием хоста. WCF не предлагает специализированного типа привязки для конечной точки обмена метаданными. Вместо этого необходимо сконструировать пользовательскую при- вязку, которая использует соответствующий транспортный элемент привязки, и передать этот элемент в параметре конструктора экземпляра пользователь- ской привязки. Далее вызывается метод AddServiceEndpoint() хоста, которому передается адрес, пользовательская привязка и тип контракта IMetadataExchange. В листинге 1.13 представлен код, необходимый для добавления конечной точки МЕХ через TCP. Обратите внимание: перед добавлением конечной точки необ- ходимо проверить наличие аспекта поведения метаданных. Листинг 1.13. Программное добавление конечной точки МЕХ для TCP Bindi ng Element bindingElement = new TcpTransportBindingElement(): CustomBinding binding = new CUstomBinding(bindingElement): Uri tcpBaseAddress = new Uri("net.tcp://localhost:9000/"): ServiceHost host = new ServiceHost(typeof(MyService).tcpBaseAddress): ServiceMetadataBehavior metadataBehavior: metadataBehavior = host.Description Behaviors Find<ServiceMetadataBenavior>(): if(metadataBehavior == null) { metadataBehavior = new ServiceMetadataBehavior(); host.Descript ion.Behaviors.Add(metadataBehavior); } host.AddServiceEndpoint(typeof(IMetadataExchange).binding,"MEX"; host,0pen(); Усовершенствование ServiceHost<T> Класс ServiceHost<T> можно усовершенствовать так, чтобы автоматизировать выполнение кода в листингах 1.11 и 1.13. ServiceHost<T> обладает логическим свойством EnableMetadataExchange, которое может использоваться для добавле- ния как поведения метаданных HTTP-GET, так и конечных точек МЕХ: public class ServiceHost<T> : ServiceHost { public bool EnableMetadataExchange {get;set:} public bool HasMexEndpoint {get;}
Привязки 47 public void AddAl1MexEndPoints(); Л ... ) Задание EnableMetadataExchange значения true добавляет аспект поведения обмена метаданными. При отсутствии конечных точек МЕХ EnabLeMetadataEx- change добавляет конечную точку МЕХ для каждой зарегистрированной схемы базового адреса. При использовании ServiceHost<T> листинги 1.11 и 1.3 сокраща- ются до следующего фрагмента: ServiceHost<MyService> host = new ServiceHost<MyService>(): host.EnableMetadataExchange = true: host.OpenO: ServiceHost<T> также содержит логическое свойство HasMexEndpoint, которое возвращает true, если служба содержит хотя бы одну конечную точку МЕХ (не- зависимо от транспортного протокола), и метод AddALLMexEndpoints(), добавляю- щий конечную точку МЕХ для каждого зарегистрированного базового адреса со схемами HTTP, TCP и IPC. В листинге 1.14 показана реализация этих мето- дов. Листинг 1.14. Реализация свойства EnableMetadataExchange и его вспомогательных методов public class ServiceHost<T> : ServiceHost ( public bool EnableMetadataExchange ( set ( i f(State == Communi cationState.Opened) ( throw new InvalidOperationException("Host is already opened"): ServiceMetadataBehavlor metadataBehavior: metadataBehavior = Description.Behaviors.Find<ServiceMetadataBehavior>(); if (metadataBehavior == null) ( metadataBehavior = new ServiceMetadataBehavior(); metadataBehavior.HttpGetEnabled = value: Description.Behaviors.Add(metadataBehavior); if(value == true) { if(HasMexEndpoint == false) { AddAl1MexEndPoints(): } } 1 get ( ServiceMetadataBehavi or metadataBehavior; metadataBehavior = Description.Behaviors.Find<ServiceMetadataBehavior>(): продолжение &
48 Глава 1. Основные сведения о WCF 1 f(metadataBehavlor == null) { return false: } return metadataBehavlor.HttpGetEnabled: } } public bool HasMexEndpoint ( get { Predicate<Serv1ceEndpo1nt> mexEndPoint = delegate(ServiceEndpolnt endpolnt) { return endpoint.Contract.ContractType == typeof(IMetadataExchange); }; return Col 1ection.Exists(Description.Endpolnts.mexEndPoint): } } public void AddAllMexEndPointsO { Debug.Assert(HasMexEndpoint == false); foreach(Ur1 baseAddress In BaseAddresses) ( BlndlngElement blndlngElement = null; swltch(baseAddress.Scheme) { case "net.tcp": { blndlngElement = new TcpTransportB1nd1ngElement(); break; } case "net.pipe": {...} case "http": (...} case "https": {...} } iftbindingElement !=null) { Binding binding = new CustomBlndlng(blndlngElement); AddServiceEndpoint(typeof(IMetadataExchange).binding."MEX"); } } } } Сначала EnableMetadataExchange проверяет, что хост еще не был открыт; для этой цели используется свойство State базового класса Communicationobject. EnableMetadataExchange не переопределяет значение, заданное в конфигурацион- ном файле, и задает его только в том случае, если аспект поведения метаданных в конфигурационном файле отсутствует. При чтении свойство проверяет, был
Привязки 49 ли аспект поведения задан ранее (то есть присутствует ли в коллекции аспек- тов поведения). Если аспект поведения не задан, EnableMetadataExchange воз- вращает false, а если задан — возвращает свойство HttpGetEnabled. Свойство HasMexEndpoint использует анонимный метод1 для инициализации предиката, который проверяет, действительно ли контракт заданной конечной точки отно- сится к типу IMetadataExchange. Затем свойство использует статический класс Collection и вызывает метод Exists() с передачей коллекции конечных точек, полу- ченной у хоста службы. Exists() вызывает предикат для каждого элемента кол- лекции и возвращает true, если хотя бы один из элементов соответствует преди- кату (то есть вызов анонимного метода вернул true), и false в противном случае. Метод AddAUMexEndPoints() перебирает элементы коллекции BaseAddresses. Для каждого найденного базового адреса создается элемент привязки с подходящим транспортом, создается пользовательская привязка, и все эти данные использу- ются для добавления конечной точки (как в листинге 1.13). Metadata Explorer Конечная точка МЕХ предоставляет метаданные, которые описывают не только контракты и операции, но и предоставляют информацию о контрактах данных, безопасности, транзакциях, надежности и сбоях. Чтобы читателю было проще Рис. 1.8. Metadata Explorer 1 Если вы не знакомы с анонимными методами, почитайте мою статью в MSDN Magazine «Create Elegant Code with Anonymous Methods, Iterators, and Partial Classes» (май 2004 г.).
50 Глава 1. Основные сведения о WCF представить метаданные службы, я написал программу Metadata Explorer (она входит в число других примеров исходного кода книги). На рис. 1.8 показано окно Metadata Explorer с конечными точками из листинга 1.7. Чтобы использо- вать Metadata Explorer, достаточно указать адрес HTTP-GET конечной точки МЕХ работающей службы. Программирование на стороне клиента Для вызова операций службы клиент должен сначала импортировать контракт службы в родное представление своей среды. Если клиент использует WCF, вызов операций чаще всего производится через посредника — класс CLR с под- держкой единственного интерфейса CLR, представляющего контракт службы. Если служба поддерживает несколько контрактов (через но крайней мере такое же количество конечных точек), клиенту потребуется отдельный посредник для каждого тина контракта. Посредник представляет те же операции, что и кон- тракт службы, но при этом содержит дополнительные методы для управления своим жизненным циклом и подключением к службе. Он полностью инкапсу- лирует все аспекты службы: ее размещение, технологию реализации, платфор- му времени выполнения и коммуникационный транспорт. Построение посредника Visual Studio 2005 позволяет импортировать метаданные службы и сгенериро- вать посредника. Если служба использует автохостинг, запустите ее, а затем выберите команду Add Service Reference в контекстном меню проекта клиента. Если хостинг службы обеспечивает IIS или WAS, заранее запускать службу не обязательно. Интересная подробность: если служба использует автохостинг в другом проекте того же решения (solution), к которому принадлежит клиент- ский проект, вы можете запустить хост в Visual Studio 2005 и добавить ссыл- ку — в отличие от большинства команд проекта, эта возможность не блокирует- ся на время отладочного сеанса (рис. 1.9). i Solution Sei Hosting* (2 protects) j - Hoel j Properties :♦! --a References I App.conlig • Д MyServiee.es ; Program cs j J3 MyCbent -an Properties , AddServiceRe«елее. MiCSent.es Proxy.es I WlWHf1 11 I Enter ths service URI and reference name and cTck OK to add al the avertable Service URI ________________________________________________________ |http://tocabostROOO/ Browse | Service reference parne- Jlocalhost OK | Caicet | Рис. 1.9. Построение посредника в Visual Studio 2005
Программирование на стороне клиента 51 На экране появляется диалоговое окно Add Service Reference, в котором необ- ходимо ввести базовый адрес службы (или базовый адрес и МЕХ UR1) и про- странство имен, в котором должен размещаться посредник. Вместо Visual Studio 2005 также можно воспользоваться утилитой команд- ной строки SvcUtil.exe. Ей передается адрес HTTP-GET или адрес конечной точки МЕХ, а также (не обязательно) имя файла посредника. По умолчанию посредник генерируется в файле output.cs, но ключ /out позволяет задать дру- гое имя. Например, если хостинг службы MyService осуществляется в I1S или WAS и при этом включен обмен метаданными через HTTP-GET, выполняется сле- дующая командная строка: SvcUti 1 http://1 оса 1 host/MyService/MyServiсе. svc /out: Proxy.cs Если вы используете хостинг HS с портом, отличным от 80 (например, 81), номер порта должен быть включен в базовый адрес: SvcUti 1 http: / /1 ос а 1 hos t: 81 /MyServ i се/ MyServ i ce. svc / out: Proxy cs При автохостинге, если служба публикует метаданные через HTTP-GET, ре- гистрирует базовые адреса и предоставляет конечные точки обмена метаданны- ми с отосительпым адресом МЕХ: http://loca1 host: 8002/ net .tcp://local host: 8003 net.pipe://local host/MyPipe После запуска хоста посредник генерируется следующими командами: SvcUtil http://localhost:8002/МЬХ SvcUtil http://localhost:8002/ SvcUti 1 net. tcp: / /1 oca 1 host: 8003 / ME X SvcUtil net pipe://localhost/MyPipe/MEX /out:Proxy.cs /out:Proxy.cs /out:Proxy.cs /out:Proxy.cs ПРИМЕЧАНИЕ-------------------------------------------------------------------- Главным преимуществом SvcUtil перед Visual Studio 2005 являются разнообразные возможности на- стройки и управления генерируемыми посредниками. * I * * * * * * В Допустим, имеется следующее определение службы: [ServiceContract (Namespace = "MyNamespace")J interface IMyContract I [Operationcontract 1 void MyMethodt); class MyService : IMyContract public void MyMethodO } (•••) SvcUtil генерирует для него посредника, приведенного в листинге 1.15. В большинстве случаев значения Action и ReplyAction можно смело удалить, поскольку используемого по умолчанию режима имени метода вполне доста- точно.
52 Глава 1. Основные сведения о WCF Листинг 1.15. Посредник [ServiceContract(Namespace = "MyNamespace")] public interface IMyContract [OperationContract(Action = "MyNamespace/IMyContract/MyMethod", ReplyAction = "MyNamespace/IMyContract/MyMethodResponse")] void MyMethod(): } public partial class MyContractClient : ClientBase<IMyContract>,IMyContract public MyContractClientO {} public MyContractClient(string endpointName) : base(endpointName) {} public MyContractClient(Binding binding.EndpointAddress remoteAddress) : base(bi ndi ng.remoteAddress) {} /* Дополнительные конструкторы */ public void MyMethodO { Channel .MyMethodO; } } При взгляде на класс посредника бросается в глаза одно удивительное об- стоятельство: он не содержат ссылок на класс реализации службы, а только на контракт, предоставляемый службой! Посредник может использоваться как в сочетании с конфигурационным файлом клиентской стороны, предоставляю- щим адрес и привязку, так и без конфигурационного файла. Учтите, что каж- дый экземпляр посредника ссылается ровно на одну конечную точку. Конечная точка, с которой осуществляется взаимодействие, передается посреднику при конструировании. Как говорилось ранее, если контракт на стороне службы не предоставляет пространства имен, по умолчанию используется пространство http://tempuri.org. Административная настройка клиента Клиент должен знать, где находится служба, использовать ту же привязку, что и служба, — и конечно, импортировать определение контракта службы. Факти- чески речь идет об информации, которая хранится в конечной точке службы. Соответственно, клиентский конфигурационный файл содержит информацию о конечных точках и даже использует ту же схему конфигурации конечных то- чек, что и хост. В листинге 1.16 приведен клиентский конфигурационный файл, необходи- мый для взаимодействия со службой, хост которой настроен в соответствии с листингом 1.6.
Программирование на стороне клиента 53 Листинг 1.16. Клиентский конфигурационный файл <system. servi ceModel > <client> <endpoint name = "MyEndpoint" address = "http://localhost:8000/MyService/" binding = "wsHttpBinding" contract = "IMyContract" /> </client> </system. servi ceModel > В клиентском конфигурационном файле может быть перечислено столько конечных точек, сколько служб он поддерживает, а клиент может использовать любую из них. В листинге 1.17 приведен клиентский конфигурационный файл для хостового конфигурационного файла из листинга 1.7. Обратите внимание: каждая конечная точка в клиентском конфигурационном файле обладает уни- кальным именем. Листинг 1.17. Клиентский конфигурационный файл с несколькими конечными точками <system. servi ceModel > <client> <endpoint name = "FirstEndpoint" address = "http://localhost:8000/MyService/" binding = "wsHttpBinding" contract = "IMyContract" /> <endpoint name = "SecondEndpoint" address = "net.tcp://localhost:8001/MyService/" binding = "netTcpBinding" contract = "IMyContract" /> <endpoint name = "ThirdEndpoint" address = "net.tcp://localhost:8002/MyService/" binding = "netTcpBinding" contract = "IMyOtherContract" /> </client> </system. servi ceModel > Конфигурация привязок Стандартные привязки на стороне клиента можно настроить в соответствии с привязками служб (по аналогии с тем, как это делается при конфигурации служб). Пример приведен в листинге 1.18. Листинг 1.18. Конфигурация привязки на стороне клиента <system. servi ceModel > <client> <endpoint name = "MyEndpoint" address = "net.tcp://localhost:8000/MyService/" bindingconfiguration « "TransactionalTCP" продолжение &
54 Глава 1. Основные сведения о WCF binding = "netTcpBinding" contract = "IMyContract" /> </cl1ent> <bindings> <netTcpBinding> <binding name = "TransactionalTCP" transactionFlow = "true" /> </netTcpBinding> </bindings> </system.serviceModel> Построение клиентского конфигурационного файла По умолчанию SvcUtil также автоматически генерирует клиентский конфигу- рационный файл с именем output.config. Вы также можете задать имя конфигу- рационного файла при помощи ключа /config: SvcUtil http://localhost:8002/MyService/ /out.-Proxy.cs /config:App.Config Ключ /noconfig запрещает построение конфигурационного файла: SvcUtil http://1 оса 1 host:8002/MyServiсе/ /out:Proxy.cs /noconfig Я не рекомендую использовать SvcUtil для построения конфигурационного файла. Дело в том, что утилита генерирует развернутые секции привязок, кото- рые обычно лишь содержат явно заданные значения по умолчанию, а это лишь приводит к загромождению конфигурационного файла. Внутрипроцессная конфигурация При внутрипроцессном хостинге клиентский конфигурационный файл также является конфигурационным файлом хоста службы. Этот файл содержит запи- си, относящиеся как к службе, так и к клиенту (листинг 1.19). Листинг 1.19. Конфигурационный файл при внутрипроцессном хостинге <system.servi ceModel> <services> <service name = "MyService"> <endpoint address = "net.pipe://localhost/MyPipe" binding = "netNamedPipeBinding" contract = "IMyContract" /> </service> </services> <client> <endpoint name = "MyEndpoint" address = "net.pipe://localhost/MyPipe" binding = "netNamedPipeBinding" contract = "IMyContract" /> </client> </system.serviceModel> Обратите внимание на использование привязки именованного канала при внутрипроцессном хостинге.
Программирование на стороне клиента 55 SvcConfigEditor В WCF входит редактор SvcConfigEditor.exe, подходящий для редактирования как хостовых, так и клиентских конфигурационных файлов (рис. 1.10). Редак- тор может запускаться из Visual Studio щелкните правой кнопкой мыши на конфигурационном файле (клиентском или хостовом) и выберите команду Edit WCF Configuration. * с Adocwnents and settmgs\idesign\desktop\deinaAessenhah\c0py of *е1^юН«пд\«ег*1Се\лрр. jEte - Mtdtotoft... - 1 Services i MyNamespace My Ser vice -J Host + । Endpoints + I Client - I Binding 5 Binding | Security | T ansactionalT CP IneU opBindtngj] t Diagnostics tI Advanced i Endpoint Behaviors - I Service Behaviors "П NewBehavior + I Extensions * I HostEnvironment CW* Binding Configuration , Create a New Service ^Create a New Client.. MaxBufferPoolSize MaxBufferSize MaxConnections M axR eceivedM essageS ize OpenTimeout PortS haringEnabled Receive? imeout SendTimeout T ransactionFlow TransactionProtocol T ransferMode S ReaderQuotaxPropertre* MaxArrayLength MaxBytesPerRead MaxDepth MaxNameT ableCharCount ♦ MaxStringContentLength El RefiairteSexsion Properties Enabled Inactivity!imeout Ordered Enabled Enables reliable sessions on this binding. 524288 6553Б 10 6553Б 00:81:00 False 00:10:00 00:01:00 True OleT ransactions Buffered 0 0 0 0 0 True 00:10:00 True Рис. 1.10. SvcConfigEditor используется для редактирования хостовых и клиентских конфигурационных файлов Лично я неоднозначно отношусь к SvcConfigEditor. С одной стороны, редак- тор хорошо работает с конфигурационными файлами, и при работе с ним разра- ботчику не обязательно учить конфигурационную схему. С другой стороны, он не избавляет от необходимости хороню понимать конфигурацию WCF, а не- сложные изменения проще внести вручную, чем прибегать к средствам Visual Studio 2005. Создание и использование посредника Класс посредника наследует от класса CLiendBase<T>, определение которого вы- глядит так: public abstract class ClientBase<T> ICommunicationObject.I Disposable protected ClientBase(string endpointName): protected ClientBase(Binding binding.EndpointAddress remoteAddress):
56 Глава 1. Основные сведения о WCF public void OpenO: public void CloseO; protected T Channel {get;} // Дополнительные члены } ClientBase<T> получает один обобщенный параметр типа, определяющий контракт службы, инкапсулируемый посредником. Свойство Channel класса ClientBase<T> относится к типу этого параметра. Сгенерированный субкласс ClientBase<T> просто делегирует Channel вызов метода (см. листинг 1.15). Чтобы использовать посредника, клиент сначала должен создать экземпляр посредника и передать конструктору информацию о конечной точке: либо имя секции из конфигурационного файла, либо адрес конечной точки и объекты привязок, если конфигурационный файл не используется. Затем клиент ис- пользует методы посредника для обращения к службе, а после завершения ра- боты со службой клиент должен закрыть экземпляр посредника. Например, для определений из листингов 1.15 и 1.16 клиент конструирует посредника с указа- нием конечной точки из конфигурационного файла, вызывает метод и закрыва- ет посредника: MyContractClient proxy = new MyContractClient("MyEndpoint"): proxy.MyMethodC); proxy.Close(); Если в клиентском конфигурационном файле определена только одна ко- нечная точка для типа контракта, используемого посредником, клиент может не указывать имя конечной точки в конструкторе посредника: MyContractClient proxy = new MyContractClient(); proxy.MyMethodC): proxy.Close(); Но если для одного типа контракта доступно несколько конечных точек, по- средник инициирует исключение. Закрытие посредника После завершения работы клиента с посредником рекомендуется всегда закры- вать посредника. В главе 4 будет показано, почему в некоторых случаях закры- тие необходимо — оно завершает сеанс работы со службой и закрывает подклю- чение. Также для закрытия посредника можно использовать метод Dispose(). Пре- имущество Dispose() заключается в том, что при помощи ключевого слова using можно обеспечить его вызов даже при возникновении исключений: using(MyContractClient proxy = new MyContractClient()) { proxy.MyMethod(); } Если контракт объявляется клиентом напрямую (вместо использования конкретного класса посредника), клиент может либо проверить поддержку I Disposable:
Программирование на стороне клиента 57 IMyContract proxy = new MyContractClient()); proxy. MyMethod(); IDIsposable disposable = proxy as {Disposable; {((disposable != null) ( disposable.Oisposet): ) либо свернуть запрос в конструкции using: IMyContract proxy = new MyContractClient(); using(proxy as IDisposable) ( proxy. MyMethodC); ) Тайм-аут вызова Каждый вызов, выданный клиентом WCF, должен быть завершен в течение времени, продолжительность которого настраивается заранее. Если по какой-ли- бо причине время обработки вызова превышает величину тайм-аута, вызов от- меняется, а клиент получает исключение TimeoutException. Точное значение тайм-аута является свойством привязки; значение по умолчанию составляет одну минуту. Чтобы установить другую величину тайм-аута, задайте свойство SendTimeout абстрактного класса Binding: public abstract class Binding : ( public TimeSpan SendTimeout (get;set;} // ... Например, при использовании WSHttpBinding: <client> <endpoint binding = "wsHttpBinding" bindingconfiguration = "LongTimeout" /> </client> <bindings> <wsHttpBi nding> <binding name = "LongTimeout" SendTimeout = "00:05:00"/> </wsHttpBi nding> </bindings> Программная настройка клиента Вместо того чтобы полагаться на содержимое конфигурационного файла, кли- ент также может на программном уровне сконструировать объекты адреса и привязки, соответствующие типу конечной точки службы, и передать их кон- структору посредника. Предоставлять контракт нет необходимости, потому что он передается посреднику в форме обобщенного параметра типа. Для представ- ления адреса клиент создает объект класса EndpointAddress:
58 Глава 1. Основные сведения о WCF public class EndpointAddress { public EndpointAddresststrmg uri); // } Этот способ продемонстрирован в листинге 1.20 для службы из листинга 1.9. Приведенный код является функциональным аналогом листинга 1.16. Листинг 1.20. Программная настройка клиента Binding wsBinding = new WSHttpBinding(); EndpointAddress endpointAddress = new EndpointAddress("http://local host:8000/MyService/"); MyContractClient proxy = new MyContractCllenttwsBinding,endpointAddress): proxy.MyMethodt): proxy.Closet): Клиент также может на программном уровне настроить свойства привязки (по аналогии с секциями привязок в конфигурационных файлах): WSHttpBinding wsBinding = new WSHttpBindlng(): wsBinding.SendTimeout = TimeSpan.FromMinutes(5): wsBinding.TransactionFlow = true: EndpointAddress endpointAddress = new EndpointAddress("http://local host:8000/MyService/"): MyContractClient proxy = new MyContractCllenttwsBinding.endpointAddress): proxy.MyMethodt); proxy.Closet): Как и прежде, обратите внимание на использование конкретного субкласса Binding для обращения к свойствам привязки. Сравнение административной и программной настройки Два представленных способа настройки клиента и службы дополняют друг друга. Административная настройка дает возможность менять важные аспекты рабо- ты службы и клиента без необходимости повторного построения или разверты- вания. Главный недостаток административной настройки заключается в том, что она небезопасна по отношению к типам, а ошибки конфигурации обнару- живаются только во время выполнения. Программная конфигурация полезна при динамическом формировании кон- фигурации (то есть во время выполнения на основании введенных данных или текущих условий), а также в статических, никогда не изменяемых конфигура- циях, которые можно жестко закодировать в программе. Например, если вас интересуют только внутрипроцессные вызовы, вы вполне можете жестко зако- дировать использование привязки NetNamedPipeBinding и ее конфигурации. Тем не менее в общем и целом большинство клиентов и служб прибегает к исполь- зованию конфигурационных файлов.
Архитектура WCF 59 Архитектура WCF К настоящему моменту я рассказал все, что необходимо знать для настройки и использования простых служб WCF. Тем не менее, как будет описано в остав- шейся части книги, WCF предлагает неимоверно полезную поддержку надеж- ности, транзакций, управления параллельным выполнением, безопасности и ак- тивизации экземпляров — все эти возможности базируются на архитектуре WCF, в основу которой заложен перехват взаимодействий. Взаимодействие клиента с посредником означает, что WCF всегда незримо присутствует между службой и клиентом, перехватывая вызовы и выполняя предварительную и завершающую обработку. Перехват начинается с того мо- мента, когда посредник сериализует кадр стека вызовов в сообщение и отправ- ляет сообщение по цепочке каналов. Канал представляет собой простой пере- хватчик, предназначенный для выполнения конкретной операции. Каналы на стороне клиента обеспечивают предварительную обработку сообщения. Точная структура и строение цепочки каналов зависит в основном от привязки. Напри- мер, один из каналов может отвечать за кодирование сообщения (двоичное, текст, МТОМ), другой! — за передачу контекста безопасности, третий — за рас- пространение клиентской транзакции, четвертый -- за управление надежным сеансом, пятый — за шифрование тела сообщения (если этот режим включен) и т. д. Последним каналом на стороне клиента является транспортный канал, который передает сообщение хосту по настроенному транспорту. На стороне хоста сообщение также проходит по цепочке каналов, обеспечи- вающих предварительную обработку сообщения перед вызовом. Первый канал на стороне хоста — транспортный канал — получает сообщение от транспорта. Последующие каналы выполняют различные операции: такие, как дешифро- вание тела сообщения, декодирование сообщения, присоединение распростра- няемой транзакции, назначение администратора безопасности, управление се- ансом и активизацию экземпляра службы. Последний канал на стороне хоста передает сообщение диспетчеру. Диспетчер преобразует сообщение в кадр сте- ка и вызывает экземпляр службы. Последовательность действий показана на рис. 1.11. Служба не знает, от какого клиента поступил вызов — локального или уда- ленного. На самом деле к ней обращается именно локальный клиент (диспет- чер). Перехват на стороне клиента и службы гарантирует, что клиент и служба получают среду времени выполнения, необходимую для их нормальной работы. Экземпляр службы выполняет вызов и возвращает управление диспетчеру, ко- торый преобразует возвращаемые данные и информацию об ошибке (если она есть) в возвращаемое сообщение. Далее весь процесс повторяется в обратном направлении: диспетчер передает сообщение по каналам на стороне хоста для выполнения завершающей обработки — управления транзакцией, деактивиза- цией экземпляра, кодирования ответа, шифрования и т. д. Возвращаемое сооб- щение переходит в транспортный канал, который отправляет его каналам сто- роны клиента для клиентской завершающей обработки. Последняя состоит из таких операций, как дешифрование, декодирование, закрепление или отмена
60 Глава 1. Основные сведения о WCF Рис. 1.11. Архитектура WCF транзакции и т. д. Последний канал передает сообщение посреднику. Посред- ник преобразует возвращаемое сообщение в кадр стека и возвращает управле- ние клиенту. Самое замечательное свойство описанной архитектуры заключается в том, что практически все ее точки предоставляют точки входа для расширения — вы можете предоставить пользовательские каналы для реализации закрытых фор- матов взаимодействия, пользовательские аспекты поведения для управления экземплярам, пользовательские аспекты безопасности и т. д. Все стандартные средства, предоставляемые WCF, реализуются на основе именно этой модели расширяемости. В книге мы неоднократно встретимся с примерами ее практи- ческого применения. Архитектура хоста Также интересно посмотреть, как происходит переход от технологически-ней- тральных, служебно-ориентированных взаимодействий к интерфейсам и клас- сам CLR. Для осуществления перехода используется хост. Каждый хостовой процесс .NET может иметь несколько прикладных доменов, а каждый приклад- ной домен содержит ноль или более экземпляров хоста службы. Однако каж- дый экземпляр хоста службы предназначается для определенного типа службы. При создании экземпляра вы фактически регистрируете экземпляр хоста служ- бы со всеми конечными точками для указанного типа на хостовом компьютере, соответствующими его базовым адресам. Каждый экземпляр хоста службы об- ладает нулем и более контекстов. Контекст (context) представляет собой внут- реннее состояние (scope) выполнения экземпляра службы. Контекст ассоци- ируется максимум с одним экземпляром службы; это означает, что контекст мо- жет быть пустым, то есть лишенным экземпляра службы. Архитектура показана на рис. 1.12.
Работа с каналами 61 Рис. 1.12. Архитектура хоста в WCF ПРИМЕЧАНИЕ------------------------------------------------------------------- Контексты WCF на концептуальном уровне сходны с контекстами Enterprise Services и контекстных объектов .NET. Именно совместная работа хоста службы и контекста обеспечивает пред- ставление родного типа CLR в виде службы. После того как сообщение будет передано по каналам, хост отображает это сообщение на новый или существую- щий контекст (с принадлежащим ему экземпляром) и поручает ему обработку вызова. Работа с каналами Каналы можно использовать для прямого вызова операций служб, без со- действия класса посредника. Класс ChannelFactory<T>, приведенный в лис- тинге 1.21 (со своими вспомогательными типами), позволяет создать посред- ника «на ходу». Листинг 1.21. Класс ChannelFactory<T> public class ContractDescription { public Type ContractType (get:set:} // public class ServiceEndpoint ( public Serv1ceEndpoint(ContractDescript1on contract.Binding binding. EndpointAddress address): public EndpointAddress Address продолжение &
62 Глава 1. Основные сведения о WCF {get:set;} public Binding Binding {get:set;} public ContractDescription Contract {get:} // ... public abstract class Channel Factory : ... { public ServiceEndpoint Endpoint {get:} // public class ChannelFactory<T> : Channel Factory.... { public ChannelFactory(ServiceEndpoint endpoint): public ChannelFactorytstring configurationName); public ChannelFactory(Binding binding.EndpointAddress endpointAddress): public static T CreateChannel(Binding binding, EndpointAddress endpointAddress); public T CreateChannel0; // Конструктору ChannelFactory<T> необходимо передать конечную точку - либо имя конечной точки из клиентского конфигурационного файла, либо объ- екты привязки и адреса, либо объект ServiceEndpoint Затем метод CreateChannel() используется для получения ссылки на посредника и использования его мето- дов. Наконец, посредник закрывается либо приведением к типу IDisposable и вызовом метода Dispose(), либо приведением к типу ICommunicationObject и вы- зовом метода Close(): ChannelFactory<IMyContract> factory = new ChannelFactory<IMyContract>(): IMyContract proxyl = factory.CreateChannel0: using(proxyl as IDisposable) { proxyl.MyMethod(): } IMyContract proxy2 = factory.CreateChannel(); proxy2.MyMethod(); ICommunicationObject channel = proxy2 as ICommunicationObject: Debug.Assert(channel !=null): channel.Close(): Также можно воспользоваться вспомогательным статическим методом CreateChannel() для создания посредника по привязки и адресу, без прямого кон- струирования экземпляра ChanneLFactory<T>: Binding binding = new NetTcpBinding(): EndpointAddress address = new EndpointAddress("net.tcp://localhost:8000"): IMyContract proxy =
йботасканалами 63 Channel Factory < I MyCont га c t>. CreateChannel (bi nch ng. address): using(proxy as {Disposable) ( proxy 1. MyMethodt): I Класс InProcFactory Для демонстрации возможностей ChannelFactory<T> рассмотрим мой статический вспомогательный класс InProcFactory, определяемый следующим образом: public static class InProcFactory ( public static I LreateInstance<S.I>() where I : class where S 1: public static void CloseProxy<I>tI instance) where I class. // И т.д 1 Класс InProcFactory предназначен для оптимизации и автоматизации внутри- процессного хостинга. Метод Createlnstance() получает два обобщенных пара- метра: тип службы S и тип поддерживаемого контракта I. Createlnstance() огра- ничивает S тинами, производными от I. Процесс использования InProcFactory предел ьно и ря м о л и н ее 11: IMyContract proxy = InProcFactory .CreateInstance<MyService, IMyCont ract>(); proxy MyMethodt): InProcFactory. C ।osePrcxy (proxy); Фактически InProcFactory берет класс службы и трансформирует его в служ- бу WCF. Это самый близкий WCF-аналог старой функции Win32 LoadLibrary(). Реализация InProcFactory<T> Все внутрипроцессные вызовы должны использовать именованные каналы с распространением транзакций. Программная настройка позволит автоматизи- ровать задание конфигурации клиента и службы, a ChannelFactory<T> поможет обойтись без посредника. В листинге 1.22 представлена реализация InProcFac- tory (для экономии места листинг слегка сокращен). Листинг 1.22. Класс InProcFactory public static class InProcFactory struct HostReco'^d public HostRecordtServiceHost host.string address) { Host = host: Address = address; public readonly ServiceHost Host: public readonly string Address: } продолжение &
64 Глава 1. Основные сведения о WCF static readonly Uri BaseAddress = new Uri("net.pipe://localhost/"): static readonly Binding NamedPipeBinding: static Dictionary<Type,HostRecord> m_Hosts = new Dictionary<Type.HostRecord>( ); static InProcFactory( ) { NetNamedPipeBinding binding = new NetNamedPipeB1nding( ); binding.TransactionFlow = true; NamedPipeBinding = binding; AppDomain.CurrentDomain.ProcessExit += delegate { foreach(KeyValuePair<Type.HostRecord* pair in m_Hosts) { pair.Value.Host.Closet ): ) ): } public static I CreateInstance<S.I>( ) where I : class where S : I { HostRecord hostRecord - GetHostRecord<S.I>( ); return ChannelFactory<I>.CreateChannel(NamedPipeBinding. new Endpoi ntAddress(hostRecord.Address)); } static HostRecord GetHostRecord<S.I>( ) where I : class where S : I { HostRecord hostRecord; i f(m_Hosts.Conta i nsKey(typeof(S))) { hostRecord - m_Hosts[typeof(S)]: } else { ServiceHost host e new ServiceHost(typeof(S). BaseAddress); string address = BaseAddress.ToString() + Guid.NewGuidO .ToString( ); hostRecord e new HostRecord(host.address); m_Hosts.Add(typeof(S).hostRecord); host.AddServi ceEndpoi nt(typeof(I).NamedPi peBi ndi ng.address); host.Open( ); } return hostRecord: } public static void CloseProxy<I>(I Instance) where I : class { ICommunicationObject proxy = instance as ICommunicationObject: Debug.Assert(proxy != null); proxy.Close( ); } } Основная трудность при написании InProcFactory заключается в том, что Createlnstance() может вызываться для создания экземпляров служб любого типа. Для каждого типа службы должен существовать ровно один соответст-
Надежность 65 вующий хост (экземпляр ServiceHost). Создание экземпляра хоста для каждого вызова - мысль неудачная. Но что делать Createlnstance() при получении запро- са на создание второго объекта для того же типа? IMyContract proxyl = InProcFactory .CreateInstance<MyService. IMyContract>(): IMyContract proxy2 = InProcFactory.CreateInstance<MyService. IMyContract>(): Задача решается созданием ассоциативного массива (словаря), отображаю- щего тип службы на соответствующий экземпляр хоста. При вызове метода Createlnstance() для создания экземпляра определенного типа метод проводит поиск в словаре с использованием вспомогательного метода GetHostRecord(), и хост создается только в том случае, если данный тип службы еще не присут- ствует в словаре. Если хост необходимо создать, GetHostRecord() на программ- ном уровне добавляет конечную точку, используя GUID как уникальное имя канала. Затем Createlnstance() берет адрес конечной точки из данных хоста и использует ChanneLFactory() для создания посредника. В своем статическом конструкторе, который вызывается при первом использовании класса, InProc- Factory подписывается на событие завершения процесса, используя анонимный метод для закрытия всех хостов при завершении процесса. Наконец, чтобы по- мочь клиентам с закрытием посредника, InProcFactory предоставляет метод CloseProxy(), который запрашивает у посредника ICommunicationObject и закрыва- ет его. Надежность В WCF и других служебно-ориентированных технологиях различаются поня- тия надежности транспорта и надежности сообщений. Надежность транспорта (например, предоставляемая TCP) обеспечивает гарантированную доставку «точ- ка-точка» на уровне сетевых пакетов, а также гарантирует порядок пакетов. На- дежность транспорта пе выдерживает потери сетевых подключений и других коммуникационных проблем. Надежность сообщений, как подсказывает само название, работает на уровне сообщений независимо от того, сколько пакетов требуется для его доставки. Надежность сообщений обеспечивает гарантированную доставку и порядок со- общений, независимо от количества посредников и сетевых переходов (хопов), необходимых для доставки сообщения от клиента к службе. Надежность сооб- щений базируется на отраслевом стандарте надежных взаимодействий, с под- держкой сеанса на транспортном уровне. Она обеспечивает повторные попытки в случае отказа транспорта (например, потери беспроводного подключения); автоматически решает проблемы перегрузки канала, буферизации сообщений и контроля передачи, а также может соответствующим образом регулировать количество сообщений. К области надежности сообщений также относится управление самим подключением посредством верификации и освобождение неиспользуемых ресурсов.
66 Глава 1. Основные сведения о WCF Привязки и надежность В WCF управление надежностью и настройка ее параметров осуществляется на уровне привязки. Конкретная привязка может поддерживать или не поддержи- вать надежный обмен сообщениями, а при наличии поддержки он может быть включен или отключен. Уровень надежности, поддерживаемый привязкой, оп- ределяется основным сценарием ее применения. В табл. 1.2 показано, какие привязки поддерживают надежность и упорядоченную доставку, а также приве- дены их состояния по умолчанию. Таблица 1.2. Надежность и привязка Название Поддержка надежности Состояние надежности по умолчанию Поддержка Состояние упоря- упорядоченной доченной доставки доставки по умолчанию BasicHttpBinding Нет — Нет — NetTcpBinding Да Выкл Да Вкл NetPeerTcpBinding Нет — Нет — NetNamedPipeBinding Нет —(Вкл) Да — (Вкл) WSHttpBinding Да Выкл Да Вкл WSFederationHttpBinding Да Выкл Да Вкл WSDualHttpBinding Да Вкл Да Вкл NetMsmqBinding Нет — Нет — MsmqlntegrationBinding Нет — Нет — Надежность не поддерживают BasicHttpBinding, NetPeerTcpBinding и две при- вязки MSMQ: NetMsmqBinding и MsmqlntegrationBinding. Это объясняется тем, что BasicHttpBinding ориентируется на унаследованные веб-службы ASMX, не обладающие надежностью. Привязка NetPeerTcpBinding проектировалась для сце- нариев широковещательной рассылки. Привязки MSMQ предназначены для вы- зовов без подключения, при которых концепция транспортного сеанса все рав- но невозможна. Надежность всегда включена у WSDualHttpBinding, чтобы поддерживать дей- ствующий канал обратного вызова на сторону клиента даже через HTTP. У NetTcpBinding и различных WS-привязок надежность отключается по умолчанию, но может быть включена. Наконец, NetNamedPipeBinding считается надежным по определению, потому что клиент всегда отделен от службы ровно одним сетевым переходом. Упорядочение сообщений Надежность сообщений также обеспечивает упорядоченную доставку — иначе говоря, сообщения обрабатываются в порядке их отправки, а не в порядке дос- тавки. Кроме того, каждое сообщение заведомо доставляется только один раз. WCF позволяет включить надежность без упорядоченной доставки; в этом случае сообщения доставляются в порядке их получения. По умолчанию для
Надежность 67 всех привязок, поддерживающих надежность, при включении надежности так- же включается и упорядоченная доставка. Настройка надежности Надежность (и упорядоченная доставка) может настраиваться как на про- граммном, так и на административном уровне. Включение надежности должно осуществляться на стороне как клиента, так и хоста службы; в противном слу- чае клиент не сможет взаимодействовать со службой. Надежность настраивает- ся только для тех типов привязок, которые ее поддерживают. В листинге 1.23 приведен конфигурационный файл на стороне службы, у которого в секции па- раметров привязки включается надежность при использовании привязок TCP. Листинг 1.23. Включение надежности для привязок TCP <system. servi ceModel > <services> <service name = "MyService"> <endpoint address = "net.tcp://localhost:8000/MyService" binding = "netTcpBinding" bindingconfiguration = "ReliableTCP" contract = "IMyContract" /> </service> </services> <bindings> <netTcpBinding> <binding name = "ReliableTCP"> <reliableSession enabled = "true’7> </binding> </netTcpBinding> </bindings> </system, servi ceModel > В том, что касается программной настройки, у привязок TCP и WS надеж- ность включается несколько разными способами. Например, конструктору при- вязки NetTcpBinding передается логический параметр для включения надежно- сти: public class NetTcpBinding : Binding.... public NetTcpBi ndi ng(... .bool reliableSessionEnabled): // Надежность может включаться только при конструировании, поэтому при программной настройке привязка должна изначально конструироваться как на- дежная: Binding reliableTcpBinding = new NetTcpBinding(... .true): NetTcpBinding также предоставляет класс ReliableSession для проверки состоя- ния надежности (с доступом только для чтения):
68 Глава 1. Основные сведения о WCF public class RellableSession { public TimeSpan InactivityTImeout {get:set;} public bool Ordered {get;set:} // } public class OptionalRellableSession : RellableSession { public bool Enabled {get;set:} // } public class NetTcpBinding : Binding.... { public OptionalRellableSession RellableSession {get:} // } Включение упорядоченной доставки Теоретически код службы и определение контракта не должны зависеть от при- вязки и ее свойств. Служба должна быть полностью изолирована от привязки, ничто в ее коде не должно относиться к привязке, а служба должна сохранять работоспособность с любыми аспектами настроенной привязки. На практике реализация службы или контракта может зависеть от упорядоченности доставки сообщений. Чтобы разработчики контракта или службы могли ограничивать на- бор допустимых привязок, в WCF определяется атрибут DeliveryRequirementsAt- tribute: [AttrlbuteUsage(Attr1buteTargets.Cl ass|AttrlbuteTargets.Interface AllowMultlple = true)] public sealed class DelIveryRequlrementsAttrlbute : Attribute.... { public Type Targetcontract {get;set:} public bool RequlreOrderedDelivery {get:set:} // } Атрибут DeliveryRequirementsAttribute может применяться на уровне службы; в этом случае он распространяется на все конечные точки службы, а не только на конечные точки, предоставляющие определенный контракт. Использование атрибута на уровне службы означает, что требование упорядоченной доставки является решением реализации. Если же атрибут применяется на уровне кон- тракта, он распространяется на все службы, поддерживающие данный кон- тракт — это означает, что требование упорядоченной доставки является плани- ровочным решением. Ограничение проверяется во время загрузки службы. Если конечная точка обладает привязкой, не поддерживающей надежность, или
Надежность 69 надежность была включена при отключении упорядоченной доставки, загрузка службы завершается неудачей с исключением InvalidOperationException. ПРИМЕЧАНИЕ---------------------------------------------------------------------- Привязка именованных каналов удовлетворяет ограничению упорядоченной доставки. Например, если вы хотите потребовать, чтобы у всех конечных точек служ- бы независимо от контракта была включена упорядоченная доставка, примени- те атрибут к классу службы: [Del iveryRequi cements (Requi reOrderedDel i very = true)] class MyService : IMyContract.IMyOtherContract (...) Задавая свойство Targetcontract, вы можете потребовать, чтобы ограничение надежной упорядоченной доставки распространялось только на конечные точ- ки службы, поддерживающие указанный контракт: [Del iveryRequi rements (Targetcontract = typeof(IMyContract). RequireOrderedDelivery = true)] class MyService : IMyContract. IMyOtherContract (...) Применяя атрибут DeliveryRequirements к интерфейсу контракта, вы устанав- ливаете ограничение для всех поддерживающих его служб: [DeliveryRequirements(RequireOrderedDel ivery = true)] [ServiceContract] interface IMyContract (...) class MyService : IMyContract (•••} class MyOtherService : IMyContract (...) По умолчанию значение RequireOrderedDelivery равно false, поэтому простое применение атрибута ни к чему не приведет. Например, следующие фрагменты эквивалентны: [ServiceContract] interface IMyContract (...) [DeliveryRequirements] [ServiceContract] interface IMyContract (...) [DeliveryRequirements(RequireOrderedDelivery = false)] [ServiceContract] interface IMyContract
Контракты служб Атрибут ServiceContract, описанный в предыдущей главе, представляет интер- фейс (или класс) как служебно-ориентированный контракт; это позволяет раз- работчику программировать на языках вроде С#, использовать такие конструк- ции, как интерфейсы, и представлять их в виде контрактов и служб WCF. В начале этой главы речь пойдет о том, как лучше преодолеть расхождение ме- жду двумя моделями программирования посредством перегрузки операций и наследования контрактов. Затем приводятся некоторые простые, но полезные рекомендации и приемы факторинга и проектирования контрактов служб. В за- вершение я покажу, как организовать программное взаимодействие с метадан- ными контрактов на стадии выполнения. Перегрузка операций Языки программирования — такие, как C++ и C# — поддерживают перегрузку методов: определение двух методов с одинаковым именем, но разными парамет- рами. Например, следующее определение интерфейса C# вполне допустимо: interface ICalculator int AddCint argl. int arg2); double Add(double argl.double arg2): Однако в мире WSDL-операций перегрузка недопустима. Соответственно следующее определение контракта компилируется, но выдает исключение InvalidOperation Exception при загрузке хоста службы: // Недопустимое определение контракта: [ServiceContract] interface ICalculator [Operationcontract] int Add(int argl.int arg2): [Operationcontract]
Перегрузка операций 71 double Add (double argl.double arg2); ) Впрочем, перегрузку операций можно реализовать вручную. Для этого в свойстве Name атрибута Operationcontract определяется псевдоним операции: [AttributeUsage(Attri buteTargets .Method)] public sealed class OperationContractAttribute : Attribute I public string Name (get:set:} // ... ) Псевдоним должен быть определен как на стороне службы, так и на стороне клиента. На стороне службы для перегружаемых операций предоставляются уникальные имена, как показано в листинге 2.1. Листинг 2.1. Перегрузка операций на стороне службы [ServiceContract] interface ICalculator ( [Operationcontract (Name = "Addlnt”)] int Add(int argl.int arg2): [OperationContract(Name = "AddDouble")] double Add (double argl,double arg2): ) Когда клиент импортирует контракт и генерирует посредника, в качестве имен импортируемых операций используются псевдонимы: [ServiceContract] public interface ICalculator ( [Operationcontract] int Addlnt(int argl.int arg2): [Operationcontract] double AddDouble(double argl.double arg2): ) public partial class Calculatorclient : ClientBase<ICalculator>,ICalculator public int Addlnt(int argl.int arg2) ( return Channel .Addlnt(argl,arg2): public double AddDouble(double argl.double arg2) return Channel .AddDouble(argl .arg2): ) // ) Клиент может использовать сгенерированного посредника и контракт в том виде, в котором они были получены, но их также можно переработать для под- держки перегрузки на стороне клиента. Переименуйте методы импортирован- ного контракта и посредника, но проследите за тем, чтобы класс посредника об- ращался к внутреннему посреднику с использованием перегруженных методов:
72 Глава 2. Контракты служб public int Add(int argl.int arg2) { return Channel.Add(argl.arg2); } Наконец, используйте свойство Name в импортированном на сторону клиен- та контракте для определения псевдонимов и перегрузки методов в соответст- вии с именами импортированных операций, как показано в листинге 2.2. Листинг 2.2. Перегрузка операций на стороне клиента [ServiceContract] public interface ICalculator { [OperationContract(Name = "Addlnt")] int Add(int argl.int arg2); [OperationContract(Name = "AddDouble")] double Add(double argl.double arg2); } public partial class CalculatorCllent : ClientBase<ICalculator>.ICalculator { public int Addfint argl.int arg2) { return Channel.Addlntfargl,arg2): } public double Addldouble argl.double arg2) { return Channel.Addlargl,arg2): } // } Теперь клиент может пользоваться преимуществами удобочитаемой и ло- гичной программной модели, основанной на применении перегруженных опе- раций: Calculatorclient proxy = new CalculatorClient(): int resultl = proxy.Add(1.2): double result2 = proxy.Add(l.0.2.0): proxy.Close(): Наследование контрактов Интерфейсы контрактов служб могут быть производными друг от друга, что позволяет разработчику определить иерархию контрактов. Однако атрибут ServiceContract не наследуется: [AttributeUsage(Inherited = false....)] public sealed class ServiceContractAttribute : Attribute {...}
Наследование контрактов 73 Соответственно на каждом уровне иерархии интерфейсов атрибут Service- Contract должен задаваться отдельно, как показано в листинге 2.3. Листинг 2.3. Иерархия контрактов на стороне службы [ServiceContract ] interface ISimpleCalculator ( [Operationcontract] int Add(int argl.int arg2); I [ServiceContract] interface IScientificCa 1 cu 1 ator : ISimpleCalculator ( [Operationcontract] int Muitiply(int argl.int arg2): ) При реализации иерархии контрактов один класс службы может реализо- вать всю иерархию, как и в классическом программировании С#: class MyCalculator IScientificCalculator ( public int AdcKint argl. int arg2) ( return argl + arg2: ) public int Multiply(int argl.int arg2) ( return arql * arg2; } ) Хост может предоставить одну конечную точку для интерфейса самого низ- кого уровня иерархии: <service name = "MyCalciilator"> <@060>endpoint address = "http://localhost :8001/MyCalculator/" binding = "basicHttpBinding" contract = "IScientificCalculator" /> </service> Иерархия контрактов на стороне клиента Когда клиент импортирует метаданные конечной точки службы, контракт ко- торой входит в иерархию интерфейсов, итоговый контракт на стороне клиента не сохраняет исходную иерархию. В него включается «сжатая» иерархия в форме одного контракта, имя которого соответствует контракту конечной точки. Один контракт содержит объединение всех операций всех интерфей- сов, ведущих к нему в иерархии, включая его собственные операции. Однако импортированное определение интерфейса хранит в свойствах Action и Re- sponseAction атрибута Operationcontract имя исходного контракта, определив- шего каждую операцию:
74 Глава 2. Контракты служб [AttrlbuteUsage(AttrlbuteTargets.Method)] public sealed class OperationContractAttribute : Attribute { public string Action {get;set;} public string ReplyAction {get;set;} // } Наконец, один класс посредника может реализовать все методы импортиро- ванного контракта. В листинге 2.4 приведены импортированный контракт и сгенерированный класс посредника для определений из листинга 2.3. Листинг 2.4. Сжатие иерархии контрактов на стороне клиента [ServiceContract] public interface IScientificCalculator { [OperationContract(Action = ”.../ISimpleCalculator/Add". ReplyAction = ”.../ISimpleCalculator/AddResponse")] int Add(int argl.int arg2); [OperationContract(Action » ./IScientificCalculator/Multiply". ReplyAction = ”.../IScientificCalculator/MultiplyResponse")] int Multiplydnt argl.int arg2); } public partial class ScientificCalculatorClient : ClientBase<IScientificCalculator>,IScientificCalcul ator ( public int Addtint argl.int arg2) (...} public int Multiplydnt argl.int arg2) {•••} // ... } Восстановление иерархии на стороне клиента Клиент может вручную восстановить иерархию контрактов, переработав опре- деления посредника и импортированного контракта. Пример показан в листин- ге 2.5. Листинг 2.5. Иерархия контрактов на стороне клиента [ServiceContract] public interface ISimpleCaiculator { [Operationcontract] public int Adddnt argl.int arg2); } public partial class SlmpleCaiculatorCllent : Cli entBase<ISimpleCaiculator>.ISimpleCalculator { public int Adddnt argl.int arg2)
Наследование контрактов 75 ( return Channel. Add(argl ,arg2): ) // ... I [ServiceContract] public interface IScientificCa 1 culator : ISimpleCalculator ( [Operationcontract] public int Muitiply(int argl.int arg2); ) public partial class SclentificCa 1 culatorCllent : ClientBase<ISc 1 enti ficCa 1 culator>. IScientificCalculator ( public int AdcKint argl.int arg2) { return Channel .Add(argl,arg2): } public int Multipoint argl,int arg2) ( return Channel.Muitiply(argl,arg2); } // 1 Используя значение свойства Action разных операций, клиент может выделить определения контрактов, входящих в иерархию контрактов служб, и предоставить определения интерфейсов и посредников — например, ISi mрLeCalculatoг и Simple- CalculatorClient в листинге 2.5. Задавать свойства Action и ResponseAction не нуж- но, их можно спокойно удалить. Затем интерфейс вручную включается в цепоч- ку наследования: [ServiceContract ] public interface IScientificCa1culator : ISimpleCalculator (...) Хотя служба представляется одной конечной точкой для нижнего интерфей- са иерархии, клиент может рассматривать ее как разные конечные точки с оди- наковым адресом, соответствующие разным уровням иерархии контрактов: <client> <@060>endpoint name = "SimpleEndpoint" address = "http://local host :8001/MyCal cul ator/" binding = "basicHttpBinding" contract = "ISimpleCalculator" /> <@060>endpoint name = "ScientificEndpoint" address - "http://localhost:8001/MyCalculator/" binding = "basicHttpBinding" contract = "IScientificCalculator" /> </client> Теперь на стороне клиента можно написать следующий код, в полной мере использующий преимущества иерархии контрактов:
76 Глава 2. Контракты служб SimplеСа1culatorCl1 ent proxyl = new SimpleCalculatorCl1ent(); proxyl.Add(1,2); proxyl.Close(): SimpleCalculatorCllent proxy2 = new SimpleCalculatorCl1ent(); proxy2.Add(3,4); proxy2.Multiply(5.6): proxy2.Close(); Преимущество рефакторинга посредников в листинге 2.5 заключается в том, что каждый уровень контрактов отделяется от более низких уровней. Теперь на стороне клиента вместо ссылки на ISimpLeCaLcuLator может передаваться ссылка на IScientificCalculator: void UseCa1culator(ISImpleCalculator calculator) {•••} ISImpleCalculator proxyl = new SimpleCalculatorCl1ent(); ISImpleCalculator proxy2 = new Sc1ent1f1cCalculatorC11ent(); IScientificCalculator ргохуЗ = new SclentificCalculatorCl lentO; SimpleCalculatorCllent proxy4 = new SimpleCalculatorCl 1ent(): ScientificCalculatorClient proxy5 = new Sc1ent1f1cCalculatorC11ent(): UseCalculator(proxyl): UseCalculator(proxy2); UseCalculator(ргохуЗ): UseCalculator(proxy4); UseCalculator(proxy5); Однако между посредниками не существует отношений типа «является ча- стным случаем». Хотя интерфейс IScientificCalculator наследует от ISimpleCalcula- tor, ScientificCalculatorClient не является SimpleCalculatorClient. Кроме того, реали- зацию базового контракта приходится повторять в посреднике субконтракта. Проблему поможет решить прием, который я называю сцеплением посредников (листинг 2.6). Листинг 2.6. Сцепление посредников public partial class SimpleCalculatorClient : Cl1entBase<IScientificCalculator<@062>.ISImpleCalculator { public int Add(Int argl.Int arg2) { return Channel.Add(argl.arg2): } //... } public partial class ScientificCalculatorClient : SimpleCaiculatorCl1 ent.IScientlflcCalculator { public Int Multlplydnt argl.Int arg2) { return Channel.Muitiply(argl.arg2); } //... }
Факторинг и проектирование контрактов служб 77 Только посредник, реализующий базовый контракт самого верхнего уровня, наследует напрямую от CLientBase<T>, а субинтерфейсу нижнего уровня он пере- дается в параметре типа. Все остальные посредники наследуют от посредника, находящегося непосредственно над ними, и соответствующего контракта. Сцепление посредников реализует связь типа «является частным случаем» между посредниками, а также обеспечивает повторное использование кода. Везде на стороне клиента, где ожидается ссылка на SimpLeCalculatorCLient, может использоваться ссылка на ScientificCaLculatorCLient: void UseCalculator(SimpleCaiculatorClient calculator) SimpleCalculatorCl ient proxy1 = new Simpl eCa 1 cul atorC 1 ient(); SimpleCalculatorClient proxy2 = new ScientificCalculatorCl ient(): Scienti ficCal cul atorCl i ent ргохуЗ = new ScientificCalculatorCl ient(): useCalculator(proxyl): useCalculator(proxy2): useCalcul ator (ргохуЗ): Факторинг и проектирование контрактов служб Довольно о синтаксисе; как проектировать контракты служб? Как определить, какие операции должны быть выделены в тот или иной контракт службы? Сколько операций должен содержать каждый контракт? Ответы на эти вопро- сы не имеют особого отношения к WCF, а скорее относятся к области абстракт- ного служебно-ориентированного анализа и проектирования. Подробное обсу- ждение разложения системы на службы и идентификации методов контрактов выходит за рамки настоящей книги. Тем не менее в этом разделе приводится ряд рекомендаций, немного упрощающих работу по проектированию контрак- тов служб. Факторинг Контракт службы представляет собой совокупность логически связанных опе- раций. Трактовка термина «логическая связь» зависит от предметной области. Контракты служб могут рассматриваться как разные аспекты некоторой сущно- сти. Когда (после анализа требований) будут идентифицированы все операции, поддерживаемые этой сущностью, необходимо распределить их по контрактам. Это называется факторингом контрактов служб. При факторинге контрактов служб всегда старайтесь мыслить категориями многократного применения. В служебно-ориентированном приложении базовой единицей многократного применения является контракт службы. Удастся ли создать в результате факто- ринга контракты, которые могут использоваться другими сущностями систе- мы? Какие аспекты сущности могут быть логически выделены для использова- ния другими сущностями?
78 Глава 2. Контракты служб Возьмем конкретный, но простой пример: допустим, мы хотим смоделиро- вать службу собаки. Требования: собака должна уметь лаять и выполнять ко- манду «апорт», получать регистрационный номер ветеринарной клиники и проходить вакцинацию. Вариант первый: определяем контракт службы IDog с разными типами служб (PoodleService, GermanShepherdService и т. д.), реализую- щими контракт IDog: [ServiceContract] Interface IDog { [Operationcontract] void FetchO: [Operationcontract] void BarkO; [Operationcontract] long GetVetClinlcNumberO; [Operationcontract] void VaccinateO: } class PoodleService : IDog {...} class GermanShepherdService : IDog {...} Однако подобную структуру контракта службы IDog нельзя признать удач- ной с точки зрения архитектуры. Хотя в контракте присутствуют все необходи- мые операции, Fetch() и Bark() логически связаны друг с другом в большей сте- пени, чем GetVetClinicNumber() и Vaccinate(). Fetch() и Bark() отражают один аспект собаки как сущности живой и активной, a GetVetClinicNumber() и VaccinateO — дру- гой аспект собаки как записи в архивах ветеринарной клиники. В другом, более правильном решении операции GetVetCLinicNumber() и VaccinateO выделяются в отдельный контракт с именем IPet: [ServiceContract] Interface IPet { [Operationcontract] long GetVetClinlcNumberO: [Operationcontract] void VaccinateO; } [ServiceContract] interface IDog { [Operationcontract] void FetchO: [Operationcontract] void BarkO:
Факторинг и проектирование контрактов служб 79 Так как аспект IPet существует независимо от аспекта I Dog, он может повтор- но использоваться и поддерживаться другими сущностями (например, кошка- ми): [ServiceContract j interface ICat ( [Operationcontract] void PurrO; [Operationcontract] void CatchMouse(): ) class PoodleService : IDog.IPet (...) class SiameseService : ICat.IPet (...) В свою очередь, факторинг позволяет отделить аспект управления ветери- нарными данными в приложении от службы (то есть собаки или кошки). Выде- ление операций в отдельный интерфейс обычно производится при наличии слабых логических связей между операциями. Однако даже в разных контрак- тах иногда встречаются идентичные операции, логически связанные со своими контрактами. Например, и собаки, и кошки способны линять и выкармливать свое потомство. С логической точки зрения линька является такой же собачьей операцией, как лай, и такой же кошачьей операцией, как мурлыканье. В подобных случаях вместо простого разделения контракты служб выстраи- ваются в иерархию: [ServiceContract] interface IMammal I [Operationcontract] void ShedFur(): [Operationcontract] void LactateO; ) [ServiceContract] interface I Dog : IMammal (••} [ServiceContract] interface ICat : IMammal (...) Метрики факторинга Как видите, правильный факторинг приводит к созданию более специализиро- ванных, оптимизированных, пригодных к повторному использованию контрак- тов с неплотным соединением; соответственно, эти преимущества распростра- няются на систему в целом. В общем случае факторинг приводит к сокращению числа операций в контрактах.
80 Глава 2. Контракты служб Однако при проектировании систем на базе служб приходится учитывать взаимодействие двух противостоящих факторов (рис. 2.1). Первый фактор - затраты на реализацию контрактов служб, а второй — затраты на их объедине- ние или интеграцию в приложение. Рис. 2.1. Баланс между количеством и размером служб Если контракты служб окажутся слишком мелкими, реализация каждого контракта упрощается, но общие затраты на интеграцию всех контрактов служб окажутся непомерно большими. С другой стороны, если вы имеете дело с не- сколькими большими, сложными контрактами, затраты на их реализацию ока- жутся чрезмерно высокими (при низких затратах на интеграцию). Связь между затратами и размером контракта службы не линейна, потому что сложность не прямо пропорциональна размеру — при увеличении размера вдвое сложность возрастает от 4 до 6 раз. Аналогично, связь между затратами на интеграцию и количеством интегрируемых контрактов служб тоже не ли- нейна, потому что количество возможных сочетаний не прямо пропорциональ- но количеству задействованных служб. В любой конкретной системе общий объем работ по проектированию и со- провождению служб, реализующих контракты, складывается из двух факто- ров — затрат на реализацию и затрат на интеграцию. Как видно из рис. 2.1, су- ществует область минимальных затрат в отношении размера и количества контрактов служб. Хорошо спроектированная система содержит не слишком много и не слишком мало служб, и эти службы не слишком малы и не слиш- ком велики. Так как проблемы факторинга не зависят от используемой технологии служб, я могу предложить ряд метрик и эмпирических правил, основанных на моем собственном опыте факторинга и проектирования крупномасштабных приложений. Контракты служб с одной операцией возможны, но лучше их избегать. Кон- тракт службы представляет аспект сущности; если аспект выражается всего од- ной операцией, он должен быть весьма тривиальным. Рассмотрите подробнее эту операцию: не использует ли она слишком много параметров? Может, она
Запросы на поддержку контрактов 81 чрезмерно укрупнена и ее следует разбить на несколько операций? Нельзя ли внести операцию в уже существующий контракт службы? Оптимальное количество членов в контрактах служб (по моему опыту и лич- ному мнению) составляет от 3 до 5. Если спроектированный вами контракт со- держит больше операций (скажем, от 6 до 9), дела идут относительно неплохо. И все же попытайтесь проанализировать операции и определить, нельзя ли их объединить друг с другом, потому что чрезмерный факторинг тоже вреден. Если контракт службы содержит 12 и более операций, вам определенно стоит поискать способы выделения операций в отдельный контракт службы или иерар- хию контрактов. Установите в своих стандартах программирования верхний порог, который не должен превышаться ни при каких условиях (например, 20). Другой принцип относится к использованию операций, напоминающих свойства: [Operationcontract j long GetVetCl imcNumber(): Подобных операций тоже следует избегать. Контракты служб позволяют клиентам вызывать абстрактные операции, не заботясь о подробностях их реа- лизации. Операции, являющиеся аналогами свойств, известны под названием минимальной инкапсуляции. Конечно, бизнес-логику задания и чтения значения переменной можно инкапсулировать на стороне службы, но в идеале клиент во- обще не должен иметь дела со свойствами. Клиент вызывает операции, а служба сама заботится об управлении своим состоянием. Взаимодействие должно вес- тись в контексте вызовов вида Выполнить действие^) — например, Vaccinate(). Как служба будет решать свою задачу, какие при этом будут задействованы пе- ременные — клиента это не касается. Небольшое предупреждение: описанные общие правила и метрики — всего лишь вспомогательный инструмент для оценки конкретной архитектуры. Ни- какое правило не заменит практического опыта в предметной области. Будьте практичны, руководствуйтесь здравым смыслом и спросите себя, что будет ра- зумно сделать в свете этих рекомендаций. Запросы на поддержку контрактов Иногда клиенту требуется на программном уровне определить, поддерживает ли некоторая конечная точка (идентифицируемая адресом) тот или иной кон- тракт. Например, представьте себе ситуацию, когда пользователь задает или на- страивает приложение, взаимодействующее со службой, в процессе установки (или даже па стадии выполнения). Если служба не поддерживает необходимые контракты, приложение должно оповестить пользователя о недопустимом адре- се и запросить другой адрес. Например, такая возможность реализована в при- ложении Credentials Manager из главы 10: пользователь должен ввести адрес службы проверки безопасности, управляющей учетными записями и ролями. Credentials Manager проверяет адрес, введенный пользователем, и убеждается в том, что адрес поддерживает необходимые контракты служб.
82 Глава 2. Контракты служб Программная обработка метаданных Чтобы приложение поддерживало такую возможность, оно должно получить метаданные конечных точек службы и проверить, поддерживает ли по крайней мере одна из конечных точек необходимый контракт. Как объяснялось в главе 1, метаданные передаются конечными точками МЕХ, поддерживаемыми службой, или по протоколу HTTP-GET. При использовании HTTP-GET адрес обмена метаданными является адресом HTTP-GET (обычно это базовый адрес службы с суффиксом ?wsdl). Для упрощения задачи разбора возвращаемых метаданных WCF предлагает группу вспомогательных классов из пространства имен System. ServiceModeLDescription, представленных в листинге 2.7. Листинг 2.7. Типы, обеспечивающие обработку метаданных public enum MetadataExchangeClientMode { MetadataExchange. HttpGet } class MetadataSet : ... {...} public class ServiceEndpointCollection : Collection<ServiceEndpoint> {...} public class MetadataExchangeClient { public MetadataExchangeClientO; public MetadataExchangeClient(Binding mexBinding); public MetadataSet GetMetadataCUri address, MetadataExchangeClientMode mode); // ... } public abstract class Metadata Importer { public abstract ServiceEndpointCollection ImportAllEndpointsO; // ... } public class WsdlImporter : MetadataImporter { public WsdlImporter(MetadataSet metadata); // ... } public class ServiceEndpoint { public EndpointAddress Address {get;set;} public Binding Binding {get;set;} public ContractDescription Contract {get;} // ... } public class ContractDescription {
Запросы на поддержку контрактов 83 public string Name (get;set:} public string Namespace (get;set:} // ... ) Класс MetadataExchangeClient может использовать привязку, связанную с об- меном метаданными в конфигурационном файле приложения. Также можно передать конструктору MetadataExchangeClient уже инициализированный экзем- пляр привязки с некоторыми измененными параметрами — например, возмож- ностью передачи сообщений большего размера, если объем возвращаемых мета- данных превышает стандартный размер сообщения. Метод GetMetadata() класса MetadataExchangeClient получает экземпляр адреса конечной точки, в котором упакован адрес МЕХ и перечисление, задающее метод доступа, а возвращает метаданные в экземпляре MetadataSet. С этим типом не следует работать напря- мую. Вместо этого создайте экземпляр субкласса Metadataimporter (например, Wsdllmporter) с передачей метаданных в параметре конструктора, после чего вы- зовите метод ImportAUEndpointsO для получения коллекции всех конечных то- чек, обнаруженных в метаданных. Конечные точки представляются классом ServiceEndpoint. ServiceEndpoint предоставляет свойство Contract типа ContractDescription, в ко- тором содержится имя и пространство имен контракта. Чтобы определить через HTTP-GET, поддерживает ли заданный базовый адрес тот или иной контракт, выполните только что описанную последователь- ность действий для получения коллекции конечных точек. Для каждой конеч- ной точки в коллекции сравните свойства Name и Namespace в ContractDescription с нужным контрактом, как показано в листинге 2.8. Листинг 2.8. Проверка контракта по адресу bool contractSupported = false: string mexAddress = "...?WSDL"; MetadataExchangeClient MEXClient = new MetadataExchangeClient(new Uri(mexAddress). MetadataExchangeClientMode.HttpGet): MetadataSet metadata = MEXC1 ient .GetMetadataO : Metadataimporter importer = new Wsdl Importer(metadata): ServiceEndpointCol lection endpoints = importer. ImportAUEndpointsO: foreach(ServiceEndpoint endpoint in endpoints) I if(endpoint.Contract.Namespace == "MyNamespace" && endpoi nt.Contract.Name == "IMyContract") contractSupported = true: break:
84 Глава 2. Контракты служб ПРИМЕЧАНИЕ-------------------------------------------------------------------- Программа MetadataExplorer, представленная в главе 1, выполняет аналогичную последователь- ность действий для получения конечных точек службы. Получая адрес на базе HTTP, программа проверяет конечные точки обмена метаданными как для HTTP, так и для HTTP-GET. Metadata Explorer также может принимать метаданные по конечным точкам, базирующимся на TCP и IPC. Ос- новной объем реализации связан с обработкой метаданных и их отображением, потому что сложная задача получения и разбора метаданных решается классами, предоставляемыми WCF. Класс MetadataHelper Я инкапсулировал и обобщил основные этапы листинга 2.8 в методе Query- Contract моего статического вспомогательного класса MetadataHelper: public static class MetadataHelper public static bool QUeryContract(string mexAddress.Type contractType): public static bool QUeryContract(string mexAddress. string contractNamespace. string contractName): // } MetadataHelper передается либо тип проверяемого контракта, либо его имя и пространство имен: string address = bool contractSupported = Metadatahelper.QueryContract(address.typeof(IMyContract)); В качестве адреса обмена метаданными MetadataHelper предоставляется ад- рес HTTP-GET или адрес конечной точки обмена метаданными для HTTP, HTTPS, TCP или IPC. В листинге 2.9 представлена реализация MetadataHelper. QueryContract() (с исключением части кода обработки ошибок). Листинг 2.9. Реализация MetadataHelper.QueryContract() public static class MetadataHelper { const int MessageMultiplir = 5: static ServiceEndpontCollection QueryMexEndpoint(string mexAddress. BindingElement bindingElement) CustomBinding binding = new CustomBinding(bindingElement); MetadataExchangeClient MEXC1lent = new MetadataExchangeClient(binding); MetadataSet metadata = MEXC1ient.GetMetadata (new EndpointAddress(mexAddress)): Metadataimporter importer = new WsdlImporter(metadata): return importer.ImportAllEndpoints(); public static ServiceEndpoint[] GetEndpoints(string mexAddress) /* Обработка ошибок */ Uri address = new Uri(mexAddress);
Запросы на поддержку контрактов 85 ServiceEndpontCollection endpoints = null: if(address. Scheme == "net. tcp") ( TcpTransportBi ndi ngElement tcpBindingElement = new TcpTransportBindingElement(): tcpBindingElement.MaxReceivedMessageSize *= MessageMultiplier; endpoints = QueryMexEndpoint(mexAddress.tcpBindingElement); } iftaddress.Scheme == "net.pipe") (...) if(address.Scheme ~ "http") // Также проверяем HTTP-GET {•••) if (address. Scheme == "https") // Также проверяем HTTP-GET return Col lection. toArray(endpoi nts): public static bool QueryContract(string mexAddress.Type contractType) г i f (contractType. Islnterface == false) { Debug Assert(false.contractType + " is not an interface"); return false; objects attributes = contractType.GetCustomAttributes( typeof(ServiceContractAttribute).false); if (attributes. Length == 0) { Debug.Assert(false,"Interface " + contractType + " does not have the ServiceContractAttribute"); return false; ServiceContractAttribute attribute = attributesEO] as ServiceContractAttribute; if(attribute.Name == null) attribute.Name = ContractType.ToString(); if(attribute.Namespace == null) attribute.Namespace = "http;//tempuri .org/"; } return QueryContract(mexAddress.attribute.Namespace.attribute.Name); } public static bool QueryContract(string mexAddress. string contractNamespace. string contractName) { i f (Stri ng. IsNullOrEmpty(contractNamespace)) Debug.Assert(false."Empty namespace"); return false; ) i f(Str i ng.IsNu11 OrEmpty (cont ract Name)) ( Debug.Assert(false."Empty name"); продолжение &
86 Глава 2. Контракты служб return false; } try { ServiceEndpoint[] endpoints - GetEndpoints(mexAddress): foreach(ServiceEndpoint endpoint in endpoints) { if(endpoint.Contract.Namespace — contractNamespace && endpoint.Contract.Name — contractName) { return true: ) } } catch {} return false: } } В листинге 2.9 схема адреса обмена метаданными разбирается методом Get- EndPoints(). В соответствии с обнаруженной транспортной схемой (например, TCP), GetEndpoints() конструирует элемент привязки для задания его свойства MaxReceivedMessageSize: public abstract class TransportBindingElement : BindingElement { public virtual long MaxReceivedMessageSize {get:set;} } public abstract class Connect!onOrientedTransportBindingElement : T ransportBi ndi ngElement {•••} public class TcpTransportBindingElement : Connect i onOri entedTransportBi ndi ngElement {...} По умолчанию значение MaxReceivedMessageSize равно 64 Кбайт. Для про- стых служб этого достаточно, но службы с большим количеством конечных то- чек генерируют большие сообщения, для которых вызов MetadataExchangeClient. GetMetadata() завершится неудачей. Мои эксперименты показали, что для боль- шинства случаев поправочного множителя 5 оказывается достаточно. Затем GetEndpoints() использует приватный метод QueryMexEndpoint() для фактическо- го получения метаданных. QueryMexEndpoint() получает адрес конечной точки обмена метаданными и используемый элемент привязки. По элементу привяз- ки конструируется пользовательская привязка, которая передается экземпляру MetadataExchangeClient; последний получает метаданные и возвращает коллек- цию конечных точек. Вместо ServiceEndpointCollection метод GetEndpoints() воз- вращает массив конечных точек с использованием моего вспомогательного клас- са Collection. Метод QueryContract() с параметром Туре сначала проверяет, что тип пред- ставляет интерфейс и помечен атрибутом ServiceContract. Поскольку атрибут ServiceContract можА использоваться для назначения псевдонима как имени,
Запросы на поддержку контрактов 87 так и пространства имен запрашиваемого типа контракта, QueryContractQ снача- ла использует их для поиска контракта. Если псевдонимы не используются, QueryContract() использует имя типа с пространством имен по умолчанию http:// tempuri.org, и вызывает версию QueryContract(), получающую имя и пространство имен. Эта версия QueryContract() вызывает GetEndpoints() для получения массива конечных точек, после чего перебирает элементы массива и возвращает true при обнаружении хотя бы одной конечной точки, поддерживающей контракт. В случае каких-либо ошибок QueryContract() возвращает false. В листинге 2.10 перечислены дополнительные методы проверки метадан- ных, присутствующие в классе MetadataHelper. Листинг 2.10. Класс MetadataHelper public static class MetadataHelper { public static ServiceEndpointE] GetEndpoints(string mexAddress); public static stringE] GetAddresses(Type bindingType. string mexAddress.Type contractType); public static stringE] GetAddresses(string mexAddress.Type contractType); public static stringE] GetAddresses(Type bindingType. string mexAddress. string contractNamespace. string contractName) where В Binding; public static stringE] GetAddresses(string mexAddress. string contractNamespace. string contractName); public static stringE] GetContracts(Type bindingType. string mexAddress). public static stringE] GetContracts(string mexAddress); public static stringE] GetOperations(string mexAddress.Type contractType); public static stringE] GetOperations(string mexAddress. string contractNamespace. string contractName); public static bool QueryContract(string mexAddress.Type contractType); public static bool QueryContract(string mexAddress. string contractNamespace. string contractName): // ) Эти мощные, полезные функции часто оказывают неоценимую помощь при настройке конфигурации, в административных программах и утилитах, при этом их реализация сводится к обработке массива конечных точек, возвращае- мого методом GetEndpoints(). Методы GetAddresses() возвращают адреса всех конечных точек, поддержи- вающих заданный контракт, или только адреса конечных точек, использующих определенную привязку. Аналогично, методы GetContracts() возвращают все контракты, поддерживае- мые всеми конечными точками, или контракты, поддерживаемые всеми конеч- ными точками с определенной привязкой. Наконец, методы GetOperations() воз- вращают все операции заданного контракта. ПРИМЕЧАНИЕ------------------------------------------------------------------------------ В главе 10 класс MetadataHelper используется в приложении Credentials Manager, а в приложе- нии Б — для администрирования подписки.
Контракты данных С абстрактной точки зрения все функции WCF сводятся к хостингу и предо- ставлению родных типов CLR (интерфейсов и классов) в виде служб, а также к использованию служб как родных интерфейсов и классов CLR. Операции служб WCF принимают и возвращают типы CLR (целые числа, строки и т. д.), а клиенты WCD передают и обрабатывают возвращаемые типы CLR. Конечно, эти типы CLR относятся к специфике .NET. Один из основополагающих прин- ципов служебно-ориентированного программирования гласит, что информация о технологии реализации службы не передается за ее пределы. В результате со службой сможет взаимодействовать любой клиент независимо от своей техно- логии реализации. Разумеется, из этого следует, что WCF не позволит выво- дить типы данных CLR через границу службы; потребуется способ преобразо- вания типов CLR в стандартное нейтральное представление и обратно. Таким представлением является простая схема на базе XML, или инфонабор (infoset). Кроме того, службе понадобится формальный механизм описания того, как должно выполняться преобразование. Этот формальный механизм, называе- мый контрактом данных, является темой настоящей главы. В первой части гла- вы будет показано, как в контексте контрактов данных организуется марша- линг и преобразования типов и как в инфраструктуре решаются проблемы иерархий классов и версий контрактов данных. Во второй части рассматривает- ся использование различных типов .NET — перечислений, делегатов, таблиц данных и коллекций — в контрактах данных. Сериализация Контракт данных является частью контрактных операций, поддерживаемых службой (другой их частью является контракт службы). Контракт данных пуб- ликуется в метаданных службы, что позволяет клиентам преобразовать ней- тральное, технологически-независимое представление в родное представление клиента. Поскольку объекты и локальные ссылки относятся к концепциям CLR, объекты и ссылки CLR не могут передаваться службам WCF и прини- маться от них. Если бы такая возможность существовала, она не только нару-
Сериализация 89 шала бы упоминавшиеся ранее принципы служебно-ориентированного про- граммирования, но и нс могла бы применяться на практике, потому что объект состоит как из данных состояния, так и из кода, управляющего ими. Передать код или логику при вызове метода C# или Visual Basic невозможно, не говоря уже о его маршалинге на другую платформу или технологию. При передаче объекта или структурного типа (value type) в параметре операции вам в дейст- вительности нужно лишь передать его состояние, а получающая сторона долж- на преобразовать его обратно к своему родному представлению. Такой подход к передаче состояния носит название маршалинга по значению. Для выполнения маршалинга по значению проще всего воспользоваться встроенной поддержкой сериализации, присутствующей в большинстве платформ (включая .NET). Рис. 3.1. Сериализация и десериализация при вызове операции На стороне клиента WCF сериализует входные параметры из родного пред- ставления CLR в инфонабор XML и пересылает их в исходящем сообщении. Как только сообщение будет получено на стороне службы, WCF десериализует его и преобразует нейтральный инфонабор XML в соответствующее представ- ление CLR, прежде чем передавать вызов службе. Затем служба обрабатывает параметры в виде родных типов CLR. После того как выполнение операции бу- дет завершено, WCF сериализует выходные параметры и возвращаемые значения в нейтральный инфонабор XML, упаковывает их в возвращаемое сообщение и от- правляет сообщение клиенту. Наконец, на стороне клиента WCF десериализует возвращаемые данные в родные типы CLR и возвращает их клиенту (рис. 3.1).
90 Глава 3. Контракты данных Сериализация .NET WCF может воспользоваться готовой поддержкой сериализации в .NET. Плат- форма .NET автоматические сериализует и десериализует объекты, применяя механизм рефлексии. Значение каждого поля объекта фиксируется и сериали- зуется с направлением в память, файл или сетевое подключение. При десериа- лизации .NET создает новый объект соответствующего типа, читает сохранен- ные значения полей и задает поля нового объекта посредством рефлексии. Так как рефлексия может работать с приватными полями, включая поля базовых классов, .NET в процессе сериализации получает полный «снимок» состояния объекта и точно воспроизводит это состояние при десериализации. .NET сериа- лизует объект в поток (stream) — логическую последовательность байтов, не зависящую от конкретного носителя информации (файл, память, коммуника- ционный порт и т. д.). Атрибут Serializable По умолчанию пользовательские типы (классы и структуры) не сериализуют- ся. Дело в том, что .NET не может определить, имеет ли смысл дамп состояния объекта в виде потока. Возможно, члены объекта обладают переходящим, неус- тойчивым состоянием (например, открытое подключение к базе данных или коммуникационному порту). Если .NET просто сериализует состояние такого объекта, то после конструирования нового объекта при десериализации из по- тока мы получим неполноценный объект. Соответственно, сериализация долж- на выполняться только с одобрения разработчика класса. Чтобы сообщить .NET о сериализуемости экземпляров класса, в определе- ние класса или структуры включается атрибут SerializableAttribute: L At t г 1buteUsaqe(At tг1buteTargets.Del egate| AttributeTargets.Enum | At Гм but eTargets .Struct | A: tributf-'Targets.Class. Irherited-fa Ise)] public sealed Nass Qena 1 vableAttribute : Attribute П Например: l Seria1iZdble] puNic class MyClass Если класс объявлен сериализуемым, .NET проверяет сериализуемость всех его полей и при обнаружении несериализуемого поля выдает исключение. Но что делать, если некоторый класс или структура содержит поле, которое не се- риализуется? Такой гии не имеет атрибута Serializable, а следовательно, вме- щающий гип тоже окажется несериализуемым. Как правило, несериализуемое поле представляет собой ссылочный тип, требующий специальной инициализа- ции. Проблема решается пометкой поля как несериализуемого и его отдельной m।iiuita/iвi3aiLiieii при десериализации. Чтобы сериализуемый тип мог содержать поле несериализуемого типа, это поле должно быть помечено атрибутом NonSerialized; например:
public class MyOtherClass (••) [Serializable] public class MyClass ( [NonSerial ized] MyOtherClass m_OtherClass: /* Методы и свойства */ ) При сериализации поля .NET сначала проверяет, не помечено ли оно атри- бутом NonSerialized. Если атрибут присутствует, .NET попросту игнорирует это поле. Это позволяет исключить из сериализации даже обычно сериализуемые типы вроде string: [Serializable] public class MyClass ( [NonSerialized] string m_Name: Форматеры .NET .NET содержит два форматера для сериализации и десериализации типов. Bina- ryFormatter сериализует данные в компактный двоичный формат, обеспечивая возможность быстрой сериализации/десериализации, потому что преобразова- ние не требует разбора. SoapFormatter использует специфический для .NET фор- мат SOAP XML; с другой стороны, сериализация потребует дополнительных затрат на формирование, а десериализация — затрат на разбор. Оба форматера поддерживают интерфейс IFormatter: public interface IFormatter object Deserial 1ze(Stream serializationstream); void Serialize(Stream serializationstream.object graph); // ... public sealed class BinaryFormatter ; IFormatter.... (...) public sealed class SoapFormatter : IFormatter.... (...) Независимо от используемого формата, кроме состояния объекта оба фор- матера сохраняют в потоке информацию о сборке и версии типа, чтобы сделать возможным его десериализацию к правильному типу. Как следствие, оба фор- матера не подходят для служебно-ориентированных взаимодействий, потому что другая сторона должна располагать сборкой типа — и конечно, в первую очередь использовать .NET. Использование Stream тоже накладывает дополни- тельные ограничения, потому что клиент и служба должны каким-то образом совместно использовать поток.
92 Глава 3. Контракты данных Форматеры WCF Из-за недостатков классических форматеров .NET в WCF пришлось включить собственный служебно-ориентированный форматер. Форматер WCF DataCon- tractSerializer способен передавать только контракт данных, без информации о нижележащих типах. DataContractSerializer определяется в пространстве имен System.Runtime.Serialization и частично представлен в листинге 3.1. Листинг 3.1. DataContractSerializer public abstract class XmlObjectSerializer { public virtual object ReadObject(Stream stream): public virtual object ReadObject(XmlReader reader): public virtual void WriteObject(XmlWriter writer.object graph): public virtual void WriteObject(Stream stream.object graph): // } public sealed class DataContractSerializer : XmlObjectSerializer { public DataContractSerializer(Type type): // } DataContractSerializer сохраняет только состояние объекта в соответствии со схемой сериализации или контрактом данных. Также обратите внимание, что DataContractSerializer не поддерживает интерфейс IFormatter. WCF использует DataContractSerializer автоматически, и разработчику нико- гда не придется иметь дело с этим классом напрямую. Тем не менее вы можете использовать DataContractSerializer для сериализации типов в поток .NET и об- ратно, по аналогии с унаследованными форматерами. В отличие от двоичного или SOAP-форматера, конструктору DataContractSerializer необходимо передать тип, с которым он должен работать, потому что сам поток данных не содержит информации о типе: MyClass objl = new MyClassO; DataContractSerializer formatter = new DataContractSerializer(typeofCMyClass)): using(Stream stream = new MemoryStream()) { formatter.WriteObject(stream.objl): stream.Seek(O.SeekOrigin.Begin): MyClass obj2 = (MyClass)formatter.ReadObject(stream); } Хотя класс DataContractSerializer может использоваться с потоками .NET, его также можно использовать в сочетании с ридерами и райтерами XML, когда единственной формой входных данных является физический XML-код (вместо файла, памяти и т. д.). Обратите внимание на использование аморфного типа object в определении DataContractSerializer в листинге 3.1. Оно подразумевает отсутствие безопасл^- сти типов на стадии компиляции, потому что конструктор может получить
Сериализация 93 один тип, WriteObject() может получить второй тип, a ReadObject() — выполнить преобразование к третьему типу. Для решения этой проблемы можно определить для DataContractSeriaLizer собственную обобщенную «обертку» — наподобие той, что представлена в лис- тинге 3.2. Листинг 3.2. Параметризованный класс DataContractSerializer<T> public class DataContractSeriа 1 izer<r> : XmlObjectSerializer ( DataContractSerial izer m_DataContractSerializer: public DataContractSerializer() ( m_DataContractSerializer = new DataContractSerializer(typeof(T)); } public new T ReadObject(Stream stream) ( return (T)m_DataContractSenal izer.ReadObject(stream): public new T ReadObject(XmlReader reader) return (T)m_DataContractSerializer.ReadObject(reader); } public void WriteObject(Stream stream.? graph) ( m_DataContractSeri al i zer. WriteObject (stream .graph); public void WriteObject(XmlWriter.T graph) { m_DataContractSerial i zer. WriteObject(writer .graph); } Обобщенный класс DataContractSeriaLizer<T> гораздо безопаснее в использова- нии, чем DataContractSeriaLizer на базе object: MyClass objl = new MyClassO; DataContractSerial izer<MyClass> formatter = new DataContractSerializer<MyClass>(): using(Stream stream = new MemoryStream()) ( formatter.Wri teObject(stream. obj 1): stream. SeekO. SeekOrigin. Begin): MyClass obj2 = formatter.ReadObject(stream); В WCF также имеется форматер NetDataContractSeriaLizer, полиморфный c IFormatter: public sealed class NetDataContractSerializer : IFormatter,... (...) Как следует из названия, форматер NetDataContractSeriaLizer, по аналогии со старыми форматерами .NET, наряду с состоянием объекта сохраняет информа- цию о типе. Используется он почти так же, как и старые форматеры:
94 Глава 3. Контракты данных MyClass objl = new MyClassO; IFormatter formatter = new NetDataContractSerializer(); us Ing(Stream stream = new MemoryStreamO) { formatter.Seri a 1i ze(stream,obj1); stream.Seek(0.SeekOrlgin.Begin); MyClass obj2 = (MyClass)formatter.Deserlallze(stream); } Класс NetDataContractSerializer проектировался как дополнение DataContract- Serializer. Например, тип можно сериализовать с использованием NetDataContract- SeriaLizer и десериализовать с DataContractSerializer: MyClass objl = new MyClassO: Stream stream = new MemoryStreamO; IFormatter formatterl = new NetDataContractSerializerO: formatterl.Serialize(stream.objl); stream.Seek(0.SeekOrigin.Begin); DataContractSerializer formatted = new DataContractSerializer(typeof(MyClass)); MyClass obj2 = (MyClass)formatter2.Read0bject(stream); stream.Closet); Данная возможность открывает пути к написанию кода, устойчивого к вер- сиям, а также миграции унаследованного кода с передачей информации о типах в служебно-ориентированные решения, в которых сохраняется только схема данных. Контракт данных и сериализация Если операция службы получает или возвращает параметр любого типа, WCF использует DataContractSerializer для сериализации и десериализации этого па- раметра. Это означает, что в качестве типа параметра или возвращаемого значе- ния операции контракта может использоваться любой сериализуемый тип — при условии, что у другой стороны ймеется определение схемы данных или контракта данных. Все встроенные примитивные типы .NET сериализуемы. На- пример, определения int и string выглядят так: [Serializable] . public struct Int32 : {••} [Serializable] public sealed class String : ... {...} Только благодаря этому обстоятельству работали все контракты служб, представленные в предыдущей главе. WCF предоставляет для примитивных типов неявный контракт данных, потому что для их схемы существует отрасле- вой стандарт. Для передачи в параметре операции пользовательского типа должны выпол- няться два условия: во-первых, тип должен быть сериализуемым, а во-вторых,
Атрибуты контракта данных 95 клиент и служба должны иметь локальное определение этого типа, которое бы приводило к той же схеме данных. Для примера возьмем контракт службы IContactManager, предназначенной для управления списком контактов: [Serializable] struct Contact ( public string FirstName: public string LastName: ) [ServiceContract] interface IContactManager ( [Operationcontract] void AddContact(Contact contact); [Operationcontract] Contact[] GetContactsO: ) Если клиент располагает эквивалентным определением структуры Contact, он сможет передать службе структуру. Эквивалентное определение может быть любым, лишь бы оно приводило к той же схеме данных при сериализации. На- пример, клиент может использовать следующее определение: [Serializable] struct Contact ( public string FirstName: public string LastName: [NonSeri al 1 zed] public string Address: ) Атрибуты контракта данных Хотя атрибут Serializable работает, для служебно-ориентированных взаимодей- ствий между клиентами и службами его возможностей недостаточно. Он поме- чает все поля типа как сериализуемые, а следовательно, являющиеся частью схемы данных типа. Гораздо лучше выглядит вариант «явного назначения»: в контракт данных включаются только те члены, которые были специально ука- заны разработчиком. Атрибут Serializable требует обязательной сериализуемо- сти данных для их передачи в параметре операции и не обеспечивает четкого разделения между возможностью использования типа в параметрах операций WCF и его сериализуемостью. Он не поддерживает ни назначения псевдони- мов для имен типов и их полей, ни отображения нового типа на заранее опреде- ленный контракт данных. Атрибут работает с полями напрямую и полностью обходит любые логические свойства, используемые для обращения к этим по- лям. Было бы лучше, если бы свойства могли добавлять свои значения при
96 Глава 3. Контракты данных обращении к полям. Наконец, Serializable не имеет прямого контроля версии - предполагается, что данные версии будут сохраняться форматерами. Соответ- ственно, по прошествии времени обновление версий усложняется. Как и прежде, на помощь приходят новые служебно-ориентированные, явно назначаемые атрибуты WCF. Первый из этих атрибутов, DataContractAttribute, определяется в пространстве имен System.Runtime.Serialization: [Attri buteUsage( Att ri buteTargets. Enum | AttributeTargets.Struct | AttributeTargets.Cl ass. Inherited=false)] public sealed class DataContractAttribute : Attribute { public string Name {get;set;} public string Namespace {get;set;} } Применение атрибута DataContract к классу или структуре не приведет к тому, что WCF начнет принудительно сериализовать его поля: [DataContract] struct Contact // Не войдет в контракт данных public string FirstName: public string LastName; } Атрибут DataContract всего лишь явно указывает, что маршалинг данного типа должен осуществляться по значению. Чтобы обеспечить сериализацию его полей, необходимо применить атрибут DataMemberAttrubute, определяемый сле- дующим образом: [Attri butellsage(Attri buteTargets.Field |Attri buteTargets.Property, Inheritedefalse)] public sealed class DataMemberAttribute : Attribute { public bool IsRequired {get;set:} public string Name {get;set;} public int Order {get;set;} } Атрибут DataMember может применяться непосредственно к полям: [DataContract] struct Contact { [DataMember] public string FirstName; [DataMember] public string LastName;
Атрибуты контракта данных 97 Он также работает и со свойствами: [DataContract] struct Contact ( string m_FirstName: string m_LastName: [DataMember] public string FirstName { get (...} set {...} ) [DataMember] public string LastName ( get (...) set ) Как и в случае с контрактами служб, видимость полей данных или самого контракта для WCF несущественна. В контракт данных могут включаться внут- ренние типы с приватными полями данных: [DataContract] struct Contact ( [DataMember] string m_FirstName: [DataMember] string m_LastName: В этой главе атрибут DataMember в основном применяется напрямую к от- крытым полям данных; это сделано для экономии места. Конечно, в реальном коде вместо открытых переменных следует использовать свойства. ПРИМЕЧАНИЕ---------------------------------------------------------------------------------------- В контрактах данных учитывается регистр символов — как на уровне типов, так и на уровне полей. Импортирование контракта данных Контракт данных, используемый в контрактных операциях, публикуется в ме- таданных службы. Когда клиент импортирует определение контракта данных, он получает определение эквивалентное, но не идентичное. Импортированное определение сохраняет исходный тип класса или структуры. Кроме того, в от- личие от контракта службы, по умолчанию в импортированном определении сохраняется исходное пространство имен типа.
98 Глава 3. Контракты данных Для примера возьмем следующее определение на стороне службы: namespace MyNamespace { CDataContract] struct Contact [ServiceContract] interface IContactManager { [Operationcontract] void AddContact(Contact contact): [Operationcontract] Contact[] GetContacts(): } } Импортированное определение будет выглядеть так: namespace MyNamespace { [DataContract] struct Contact {} } [ServiceContract] interface IContactManager { [Operationcontract] void AddContact(Contact contact): [Operationcontract] Contacts GetContactsO: } Чтобы переопределить значение по умолчанию и предоставить для контрак- та данных альтернативное пространство имен, достаточно присвоить значение свойству Namespace атрибута DataContract. Возьмем следующее определение на стороне сервера: namespace MyNamespace [DataContract(Namespace * "MyOtherNamespace")] struct Contact {...} } Импортированное определение выглядит так: namespace MyOtherNamespace [DataContract] struct Contact
Атрибуты контракта данных 99 Импортированное определение всегда имеет свойства, помеченные атрибу- том DataMember, даже если исходный тип на стороне службы не определял ни- каких свойств. Если в исходном определении на стороне службы атрибут Data- Member применялся напрямую к полям, в импортированном определении типа для обращения к ним будут использоваться свойства, имена которых состоят из имени поля данных и суффикса Field. Например, для следующего определения на стороне службы: [DataContract] struct Contact [DataMember] public string FirstName: [DataMember] public string LastName: } импортированное определение на стороне клиента будет выглядеть так: [DataContract] public partial struct Contact { string FirstNameField; string LastNameField: [DataMember] public string FirstName ( get ( return FirstNameField; ) set { FirstNameField = value; ) } [DataMember] public string LastName { get ( return LastNameField; ) set { LastNameField = value: } } Конечно, клиент может вручную переработать любое важное определение и привести его в полное соответствие с определением на стороне службы.
100 Глава 3. Контракты данных ПРИМЕЧАНИЕ--------------------------------------------------------------------------- Даже если атрибут DataMember на стороне службы применяется к приватному полю или свойству [DataContract] struct Contact { [DataMember] string FirstName {get;set;} [DataMember] string LastName; } импортированное определение все равно будет содержать открытое свойство. Если атрибут DataMember применяется к свойству, входящему в контракт данных на стороне службы, импортированное определение будет обладать идентичным набором свойств. Свойство на стороне клиента будет работать с полем, имя которого состоит из имени свойства и суффикса Field. Например, для следующего контракта данных на стороне службы: [DataContract] public partial struct Contact { string m_F1rstName; string m_LastName; [DataMember] public string FirstName { get { return m_FirstName: } set { m_FirstName = value: } } [DataMember] public string LastName { get { return m_LastName; } set { m_LastName = value: } } } импортированное определение будет выглядеть так:
Атрибуты контракта данных 101 [DataContract] public partial struct Contact { string FirstNameField; string LastNameField: [DataMember] public string FirstName ( get ( return FirstNameField: } set FirstNameField = value; ) } [DataMember] public string LastName ( get ( return LastNameField; set ( LastNameField = value: } ) ) Если атрибут DataMember применяется к свойству (на стороне службы или клиента), это свойство должно обладать методами доступа get и set. Без них при вызове произойдет исключение InvalidDataContractException. Это объясняет- ся тем, что когда свойство само является полем данных, WCF использует его при сериализации и десериализации, давая вам возможность применить любую пользовательскую логику. ВНИМАНИЕ ------------------------------------------------------------------------------------------- Не применяйте атрибут DataMember как к свойству, так и к полю, на котором оно базируется, — это приведен к дублированию полей на импортирующей стороне. Важно понять, что рассмотренные правила использования атрибута DataMem- ber действуют как на стороне службы, так и на стороне клиента. Когда клиент использует атрибут DataMember (и другие сопутствующие атрибуты, упоминае- мые в этой главе), он влияет на контракт данных, используемый для сериализа- ции и отправки параметров службе или десериализации и получения возвра- щаемых службой данных. Две стороны могут использовать эквивалентные, но не идентичные контракты данных — и как вы вскоре убедитесь, даже неэквива- лентные контракты Клиент управляет своими контрактами данных и настраи- вает их независимо от службы.
102 Глава 3. Контракты данных Контракт данных и атрибут Serializable Служба по-прежнему может использовать тип, помеченный только атрибутом Serializable: [Serializable] struct Contact { string m_FirstName; public string LastName: } При импортировании метаданных такого типа в импортированном опреде- лении будет использоваться атрибут DataContract. Кроме того, поскольку атри- бут Serializable распространяется только на поля, все будет происходить так, словно каждый сериализуемый член класса (открытый или приватный) являет- ся полем данных; в результате мы получаем набор свойств-«оберток», имена которых совпадают с именами исходных полей: [DataContract] public partial struct Contact { string LastNameField: string m_FirstNameField: [DataMember(...)] public string LastName { ...II Обращение к LastNameField } [DataMember(...)] public string m_FirstName { ... 11 Обращение к m_FirstNameField } } ПРИМЕЧАНИЕ------------------------------------------------------------------------------ Тип, помеченный только атрибутом DataContract, не может сериализоваться с использованием ста- рых форматеров. Если вы захотите сериализовать тип, следует применить к нему как атрибут Data- Contract, так и Serializable. Канальное представление такого типа выглядит так, как если бы был применен только атрибут DataContract, но к членам типа все равно должен применяться атрибут DataMember. КОНТРАКТ ДАННЫХ И СЕРИАЛИЗАЦИЯ XML-------------------------------------------------- .NET поддерживает еще один механизм сериализации — сериализацию в физический код XML с ис- пользованием специального набора атрибутов. Когда вы имеете дело с типом данных, требующим явного контроля над сериализацией XML, используйте атрибут XmlSerializerFormatAttribute с отдель- ными операциями в определении контракта — тем самым вы указываете WCF на необходимость ис- пользования сериализации XML во время выполнения. Если такая форма сериализации необходима для всех операций контракта, воспользуйтесь ключом /serializer:XmlSerializer утилиты SvcUtil, чтобы атрибут XmlSerializerFormat автоматически применялся ко всем операциям всех импортированных контрактов. Будьте внимательны с этим ключом: он влияет на все контракты данных, включая те, которые не требуют явного управления сериализацией XML.
Атрибуты контракта данных 103 Аналогичным образом клиент может использовать атрибут Serializable со своим контрактом данных, в результате чего канальное представление послед- него (то есть способ его маршалинга) будет идентично только что описанному. Составные контракты данных При определении контракта данных атрибут DataMember может применяться к полям, которые сами являются контрактами данных, как показано в листин- ге 3.3. Листинг 3.3. Составной контракт данных [DataContract] struct Address ( [DataMember] public string Street; [DataMember] public string City; [DataMember] public string State: [DataMember] public string Zip; [DataContract] struct Contract [DataMember] public string FirstName: [DataMember] public string LastName; [DataMember] public Address Address: } Возможность вложения контрактов данных демонстрирует тот факт, что контракты данных в действительности рекурсивны по своей природе. При се- риализации составного контракта данных DataContractSerializer отслеживает все ссылки в графе объекта и сохраняет их состояние. Публикация составного кон- тракта данных означает публикацию всех его составляющих контрактов. На- пример, для определения из листинга 3.3 метаданные следующего контракта службы: [ServiceContract] interface IContactManager ( [Operationcontract] void AddContact(Contact contact):
104 Глава 3. Контракты данных [Operationcontract] Contacts GetContacts(): } будут включать и определение структуры Address. События контрактов данных В .NET 2.0 появилась поддержка событий сериализации для сериализуемых ти- пов, a WCF предоставляет аналогичную поддержку для контрактов данных. При выполнении сериализации и десериализации WCF вызывает заданные ме- тоды. Всего определено четыре события сериализации/десериализации. Собы- тие serializing инициируется непосредственно перед выполнением сериализа- ции, а событие serialized — сразу же после нее. Аналогично, событие deserializing инициируется перед десериализацией, а событие deserialized — после ее завер- шения. Методы, используемые в качестве обработчиков событий сериализации, задаются с использованием атрибутов методов, как показано в листинге 3.4. Листинг 3.4. Применение атрибутов событий сериализации [DataContract] class MyDataContract ( [OnSerializing] void OnSerializing(Streamingcontext context) {••} [OnSerializing] void OnSerialized(Streamingcontext context) [OnDeserializing] void OnDeserializing(StreamingContext context) {•••} [OnDeserializing] void OnDeserializing(StreamingContext context) {•••} // } Любой метод обработки события сериализации должен обладать следующей сигнатурой: void <имя_метода>(StreamingContext context): Данное требование является обязательным, потому что во внутренней реа- лизации WCF использует делегатов для подписки и вызова методов обработки событий. Если атрибуты применяются к методам с несовместимой сигнатурой, WCF выдает исключение. Структура Streamingcontext передает информацию типу о причине его сериа- лизации, но для контрактов данных WCF она может игнорироваться. Атрибуты событий определяются в пространстве имен System.Runtime.Serialization.
Атрибуты контракта данных 105 Как подсказывают имена атрибутов, атрибут OnSerializing определяет метод обработки события serializing, атрибут OnSerialized — метод обработки события serialized и т. д. На рис. 3.2 изображена диаграмма операций, на которой показан порядок инициирования событий при сериализации. Рис. 3.2. Последовательность событий при сериализации Сначала WCF инициирует событие serializing, что приводит к вызову соот- ветствующего обработчика события. Затем WCF сериализует объект, после чего инициируется событие serialized с вызовом соответствующего обработчика событий. На рис. 3.3 представлен порядок событий при десериализации. Рис. 3.3. Последовательность событий при десериализации Обратите внимание: для вызова метода обработки события десериализации WCF необходимо сначала сконструировать объект — тем не менее WCF при этом не вызывает конструктор по умолчанию класса контракта данных. ВНИМАНИЕ ------------------------------------------------------------------ WCF не разрешает применять один атрибут события сериализации к разным методам типа контрак- та данных. Это весьма печально — данное обстоятельство исключает поддержку частичных типов, когда каждая часть имеет дело со своими событиями сериализации.
106 Глава 3. Контракты данных Использование события deserializing В процессе десериализации конструктор не вызывается, поэтому на логическом уровне обработчик события deserializing может рассматриваться как конструк- тор десериализации. Метод предназначен для выполнения любых пользова- тельских действий, предшествующих десериализации, — чаще всего инициали- зации членов классов, не помеченных как поля данных. Любые попытки задать зна- чение членов, помеченных как поля данных, будут напрасными, потому что WCF снова задаст их при десериализации с использованием данных из сообщения. Среди других действий, выполняемых в методе обработки события deserializing, можно отметить задание переменных окружения, выполнение диагностики или вы- дачу сигналов для глобальных событий синхронизации. Использование события deserialized Событие deserialized позволяет контракту данных инициализировать члены, не являющиеся полями данных, с использованием уже десериализированных зна- чений. Листинг 3.5 демонстрирует сказанное: в нем событие используется для инициализации подключения к базе данных. Без обработки события правиль- ная работа контракта данных была бы невозможна. Листинг 3.5. Инициализация несериализуемых ресурсов с использованием события deserialized [DataContract] class MyDataContract { IDbConnectlon m_Connection; [OnDeserialized] void OnDeserlal 1 zed(Streamingcontext context) { m_Connection = new SqlConnect1on(...); } /* Поля данных */ } Иерархия контрактов данных Класс контракта данных может быть субклассом другого класса контракта дан- ных. WCF требует, чтобы для каждого уровня иерархии классов контракт дан- ных был обозначен явно, потому что атрибут DataContract не наследуется: [DataContract] class Contact { [DataMember] public string FirstName; [DataMember] public string LastName;
Иерархия контрактов данных 107 (DataContract] class Customer : Contact I [DataMember] public int OrderNumber I Если на каком-либо уровне иерархии классов будет отсутствовать пометка сериализуемости или контракта данных, при загрузке службы произойдет ис- ключение InvalidDataContract. WCF допускает смешанное использование атри- бутов Serializable и DataContract в иерархии классов: [Serializable] class Contact (...) [DataContract] class Customer : Contact (...) Но обычно атрибут Serializable, если он присутствует, располагается на кор- невом уровне иерархии, потому что новые классы должны использовать атри- бут DataContract. При экспортировании иерархии контрактов данных метадан- ные содержат иерархическую информацию, а при использовании субкласса в контракте службы экспортируются все уровни иерархии классов: [ServiceContract] interface IContactManager ( [Operationcontract] void AddCustomer (Customer customer); // Contact тоже экспортируется ) Известные типы Хотя такие языки, как С#, позволяют использовать субкласс вместо базового класса, для операций WCF это не так. По умолчанию субкласс класса контрак- та данных не может использоваться вместо базового класса. Возьмем следую- щий контракт службы: [ServiceContract] interface IContactManager И Здесь нельзя получать объект Customer: [Operationcontract] void AddContact(Contact contact); 11 Здесь нельзя возвращать объекты Customer: [Operationcontract] Contacts GetContactsO: Предположим, клиент также определяет класс Customer: [DataContract] class Customer : Contact
108 Глава 3. Контракты данных [DataMember] public int OrderNumber; } Следующий фрагмент компилируется, но во время выполнения происходит ошибка: Contract contact = new CustomerO; contact.FIrstName = "Juval": contact.LastName = "Lowy": ContactManagerCHent proxy = new ContactManagerCllento: // Попытка вызова службы завершается неудачей proxy.AddContact(contact); proxy.Closet) Дело в том, что при передаче объекта Customer вместо Contact, как в предыду- щем примере, служба не знает, как десериализовать полученный объект — ей ничего не известно об объекте Customer. Аналогично, при возвращении Customer вместо Contact клиент не будет знать, как десериализовать полученный объект, потому что он знает только о Contact, но не о Customer: ///////////////// Сторона службы ///////////////// [DataContract] class Customer : Contact { [DataMember] public Int OrderNumber: } class CustomerManager : IContactManager { L1st<Customer> m_Customers = new L1st<Customer>(): public Contacts GetContactst) { return m_Customers.ToArray(); } // Продолжение реализации } ///////////////// Сторона клиента ///////////////// ContactManagerCllent proxy = new ContactManagerCllent; // Попытка вызова завершается неудачей Contacts contacts = proxy.GetContacts(): proxy.Close(): Проблема решается явной передачей WCF информации о классе Customer. Для этой цели используется атрибут KnownTypeAttribute, определяемый следую- щим образом: [Attrlbutellsage(AttrlbuteTargets.Struct | AttrlbuteTargets.Cl ass, AllowMultiple = true)] public sealed class KnownTypeAttribute : Attribute { public KnownTypeAttr1bute(Type type): // }
ДЙрархия контрактов данных 109 Атрибут KnownType позволяет назначить допустимые субклассы для кон- тракта данных: ^taContract] ЦЬюилТуре (typeof (Customer)) ] class Contact {...} BfetaContract ] class Customer : Contact (...) На стороне хоста действие атрибута KnownType распространяется на все кон- тракты и операции с использованием базового класса, на все службы и конеч- ные точки, позволяя им принимать субклассы вместо базовых классов. Кроме того, субкласс включается в метаданные, чтобы клиент имел собственное опре- деление субкласса и мог передавать субкласс вместо базового класса. Если кли- ент применит атрибут KnownType к своей копии базового класса, он сможет по- лучать от службы объекты субкласса. Известные типы уровня служб Недостаток атрибута KnownType состоит в том, что он может иметь излишне ши- рокую область действия. WCF также предоставляет атрибут ServiceKnownType- Attribute, определяемый следующим образом: [AttrlbuteUsateCAttributeTargets. Interface | AttributeTargets.Method j AttributeTargets.Class. AllowMultiple = true)] public sealed class ServiceKnownTypeAttribate : Attribute { public ServiceKnownTypeAttribute(Type type); // ... ) В отличие от применения атрибута KnownType к базовому контракту данных, при применении атрибута ServiceKnownType к конкретной операции на стороне службы известный субкласс применяется только к этой операции (по всем под- держиваемым службам): [DataContract] class Contact [DataContract] class Customer : Contact (...) [ServoceContract] interface IContactManager [Operationcontract] [ServiceKnownType(typeof (Customer))]
110 Глава 3. Контракты данных void AddContact(Contact contact); [Operationcontract] Contacts GetContactsO; } Другим операциям субкласс передаваться не может. Если атрибут ServiceKnownType применяется на уровне контракта, все опера- ции этого контракта могут принимать известный субкласс по всем реализациям служб: [ServiceContract] [ServiceKnownType(typeof(Customer))] interface IContactManager { [Operationcontract] void AddContact(Contact contact) [Operationcontract] Contactp[] GetContactsO; } ВНИМАНИЕ -------------------------------------------------------------------------------- He применяйте атрибут ServiceKnownType к самому классу службы. Хотя такой код компилируется, он возможен только в том случае, если контракт службы не определяется в виде интерфейса — а по- ступать подобным образом я настоятельно не рекомендую. Если применить атрибут ServiceKnown- Type к классу службы при отдельном определении контракта, он ни на что не повлияет. Независимо от того, применяется ли атрибут ServiceKnownType на уровне операции или на уровне контракта, экспортированные метаданные и сгенериро- ванный посредник не содержат информации о нем и включают атрибут Known- Type только для базового класса. Например, для следующего определения на стороне службы: [ServiceContract] [Servi ceKnownType(typeof(Customer))] interface IContactManager импортированное определение будет выглядеть так: [DataContract] [KnownType(typeof(Customer))] class Contact {...} [DataContract] class Customer : Contact {...} [ServiceContract] interface IContactManager Вы можете вручную отредактировать класс посредника на стороне клиента, чтобы он правильно отражал семантику стороны службы — удалите атрибут KnownType из базового класса и примените атрибут ServiceKnownType на соответ- ствующем уровне контракта.
Иерархия контрактов данных 111 Определение нескольких известных типов Оба атрибута, KnownType и ServiceKnownType, могут применяться многократно для передачи WCF информации о необходимом количестве известных ти- пов: [DataContract] class Contact (••) [DataContract] class Customer : Contact (•••I [DataContract] class Person : Contact (...) [ServiceContract] [Servi ceKnownTy pe (ty peof (Customer)) ] [Servi ceKnownType (typeof (Person)) ] interface IContactManager (...) Настройка известных типов Главный недостаток атрибутов известных типов заключается в том, что служба или клиент должны заранее знать все возможные субклассы, которые могут быть использованы другой стороной. При добавлении нового субкласса прихо- дится вносить изменения в код, перекомпилировать и развертывать его заново. Для решения этой проблемы WCF позволяет настраивать известные типы в конфигурационном файле службы или клиента, как показано в листинге 3.6. В файле должно задаваться не только имя типа, но и полное имя вмещающей сборки. Листинг 3.6. Известные типы в конфигурационном файле <system. runtime, seri al ization> <dataContractSeri al i zer> <declaredTypes> <add type - "Contact.Host.Version*!.0.0.0,Culture=neutral, PublicKeyToken=null"> <knownType type = "Customer,MyClassLibrary,Version=l.0.0.0, Culture=neutral,PublicKeyToken=null”> </add> </declaredTypes> </dataCont ractSer i a 1 i zer> </system. runtime. seri a 1 i zat i on> Интересная подробность: объявление известного типа в конфигурационном файле — единственный способ добавления известного типа, внутреннего по от- ношению к другой сборке.
112 Глава 3. Контракты данных Объект и интерфейсы Базовым типом структуры или класса контракта данных может быть интер- фейс: interface IContact { string FirstName {get;set:} string LastName {get:set:} } [DataContract] Class Contact : IContact {•} Базовый интерфейс может использоваться в контракте службы или в полях контракта данных, при условии, что атрибут ServiceKnownType будет использо- ваться для обозначения фактического типа данных: [ServiceContract] [Servi ceKnownT уре(typeof(Contact))] interface IContactManager { [Operationcontract] void AddContact(IContact contact): [Operationcontract] IContact[] GetContactsO: } Атрибут KnownType не может применяться к базовому интерфейсу, потому что сам интерфейс не будет включен в экспортированные метаданные. Вместо этого экспортированный контракт службы базируется на object, а структура или субкласс контракта данных включается без наследования: // Импортированные определения: [DataContract] class Contact {...} [ServiceContract] public interface IContactManager { [Operationcontract] [Servi ceKnownType(typeof(Contact))] [ServiceKnownType(typeof(object[]))] void AddContact(IContact contact): [Operationcontract] [Servi ceKnownType(typeof(Contact))] [ServiceKnownType(typeof(object[]))] object[] GetContactsO: } В импортированном определении атрибут ServiceKnownType всегда применя- ется на уровне операции, даже если изначально он определялся на уровне кон-
Иерархия контрактов данных 113 тракта. Кроме того, каждая операция включает объединение всех атрибутов ServiceKnownType, необходимых для всех операций. Вы можете вручную перера- ботать импортированное определение, чтобы в нем остались только необходи- мые атрибуты ServiceKnownType: [DataContract] class Contact [ServiceContract] public interface IContactManager ( [Operationcontract] [ServiceKnownType(typeof (Contact)) ] void AddContact(object contact); [Operationcontract] [Servi ceKnownType (typeof (Contact)) ] object[] GetContactsO; Если на клиентской стороне имеется определение базового интерфейса, оно может использоваться вместо object как дополнительная мера безопасности ти- пов - при условии, что в контракт данных будет добавлено наследование от ин- терфейса: [DataContract] class Contact : IContact [ServiceContract] public interface IContactManager [Operationcontract] [Servi ceKnownType (typeof (Contact)) ] void AddContact(IContact contact); [Operationcontract] [Servi ceKnownType (typeof (Contact)) ] IContact[] GetContactsO; Тем не менее в импортированном контракте заменить object конкретным типом контракта данных невозможно, потому что эти типы стали несовмести- мыми: // Недействительный контракт на стороне клиента [ServiceContract] public interface IContactManager ( [Operationcontract] void AddContact(Contact contact); [Operationcontract] Contact[] GetContactsO;
114 Глава 3. Контракты данных Эквивалентность контрактов данных Два контракта данных считаются эквивалентными, если они обладают одинако- вым канальным представлением. Такая ситуация возможна при определении одного типа (но не обязательно в одинаковых версиях), или если два контракта данных относятся к двум разным типам с одинаковыми именами контракта и членов. Эквивалентные контракты данных взаимозаменяемы: WCF позволя- ет любой службе, определенной с одним контрактом данных, работать с эквива- лентным контрактом данных. Самым распространенным способом определения эквивалентного контракта данных является связывание двух контрактов данных посредством свойства Name атрибута DataContract или DataMember. В случае атрибута DataContract свой- ство Name по умолчанию совпадает с именем типа, поэтому следующие два оп- ределения тождественны: [DataContract] struct Contact {...} IDataContract(Name - "Contact")] struct Contact {...} В действительности полное имя контракта данных всегда включает про- странство имен, но как было показано ранее, пространство имен можно изме- нить. В случае атрибута DataMember свойство Name по умолчанию содержит имя поля, поэтому следующие два определения тождественны: [DataMember] public string FirstName: [DataMember(Name = "FirstName")] public string FirstName: Присваивая альтернативные имена контракту и полям, можно сгенериро- вать эквивалентный контракт данных на базе другого типа. Например, следующие два контракта данных эквивалентны: [DataContract] struct Contact { [DataMember] public string FirstName: [DataMember] public string LastName: } [DataContract(Name - "Contact")] struct Person { [DataMember(Name « "FirstName")] public string Name: [DataMember(Name e "LastName")]
Эквивалентность контрактов данных 115 public string Surname: ) Кроме тождественности имен, должны совпадать типы полей данных. ПРИМЕЧАНИЕ-------------------------------------------------------------- Класс и структура, поддерживающие одинаковый контракт данных, взаимозаменяемы. Порядок сериализации Эквивалентные контракты данных должны сериализовать и десериализовать свои поля данных в одинаковом порядке. По умолчанию порядок сериализации внутри типа определяется алфавитным порядком, а в иерархиях классов ис- пользуется порядок «сверху вниз». При несоответствии порядка сериализации поля инициализируются значениями по умолчанию. Например, при сериализа- ции экземпляра класса Customer, определяемого следующим образом: [DataContract] struct Contact [DataMember] public string FirstName: [DataMember] public string LastName: ) [DataContract] class Customer : Contact f [DataMember] public int CustomerNumber: ) поля будут сериализованы в следующем порядке: FirstName, LastName, Customer- Number. Но при этом возникает одна проблема: объединение иерархии контрактов данных с псевдонимами контрактов и полей может привести к нарушению по- рядка сериализации. Например, следующий контракт данных уже не эквива- лентен контракту Customer: [DataContract(Name - "Customer")] public class Person [DataMember (Name = "FirstName")] public string Name: [DataMember(Name - "LastName")] public string Surname: [DataMember] public int CustomerNumber: ) поскольку сериализация выполняется в порядке CustomerNumber, FirstName, LastName. Для разрешения конфликта необходимо передать WCF порядок
116 Глава 3. Контракты данных сериализации, задавая свойство Order атрибута DataMember. Свойство Order по умолчанию равно - 1 (признак стандартного порядка сериализации в WCF), но ему можно задать значения, определяющие нужный порядок: [DataContract(Name = "Customer")] public class Person { [DataMember(Name = "FirstName".Order - 1)] public string Name; [DataMember(Name = "LastName".Order 2)] public string Surname; [DataMember(Order = 3)] public int CustomerNumber; } Изменение версий1 Служба должна быть по возможности отделена от клиента, особенно в том, что касается версии и технологии. Любая версия клиента должна быть способна ис- пользовать любую версию службы, не прибегая к проверке номера версии (на- пример, по данным из сборки), потому что эта информация специфична для .NET. Когда клиент и служба используют общий контракт данных, очень важно предоставить возможность клиенту и службе развивать свои версии контракта данных по отдельности, то есть независимо друг от друга. Чтобы подобное раз- деление стало возможным, WCF необходимо обеспечить как прямую, так и об- ратную совместимость без обмена информацией о типах или версиях. Сущест- вуют три основных сценария изменения версий: О появление новых полей; О исключение существующих полей; О передачи с промежуточным изменением версии, когда новая версия переда- ется старой версии, а потом принимается от нее (требует как прямой, так и обратной совместимости). По умолчанию контракты данных терпимы к изменению версий, а несовмес- тимости игнорируются без выдачи ошибок. Появление новых полей Самым распространенным изменением, выполняемым с контрактами данных, является добавление новых полей на одной из сторон и последующая отправка нового контракта другой стороне. DataContractSerializer просто игнорирует новые 1 Versioning. Привычные переводы: «контроль версий» и «отслеживание версий» здесь не годятся - ведь речь как раз и идет об «отвязке» клиента от службы с независимым изменением версий. — Примеч. перев.
Изменение версий 117 поля при десериализации типа. В результате как служба, так и клиент могут принимать данные с новыми полями, не входящими в исходный контракт. На- пример, служба может быть построена на базе следующего контракта данных: [DataContract] struct Contact ( [DataMember] public string FirstName: [DataMember] public string LastName: ) При этом клиент может передавать ей информацию, соответствующую сле- дующему контракту данных: [DataContract] struct Contact f [DataMember] public string FirstName: [DataMember] public string LastName; [DataHenber] public string Address; ) Подобное добавление новых полей, игнорируемых при десериализации, на- рушает совместимость схемы контракта данных, так как служба (или клиент), совместимая с одной схемой, ни с того ни с сего оказывается совместима с но- вой схемой. Исключение существующих полей По умолчанию WCF позволяет любой из сторон исключать поля из контракта данных. Данные сериализуются без отсутствующих полей и отправляются дру- гой стороне, которая ожидает получить отсутствующие поля. Намеренное уда- ление полей встречается довольно редко, более распространен другой сцена- рий - когда клиент, написанный для старого определения контракта данных, взаимодействует со службой, написанной для нового определения того же кон- тракта, содержащего дополнительные поля. Когда DataContractSerializer на сто- роне получателя не находит в сообщении информацию, необходимую для десе- риализации новых полей, он задает им значения по умолчанию, то есть null для ссылочных типов или нулевой заполнитель для структурных типов. Все выгля- дит так, как если бы отправляющая сторона вообще не инициализировала эти поля. Такая политика дает службе возможность принимать данные с отсутст- вующими полями или возвращать клиенту данные с отсутствующими полями. Эта возможность продемонстрирована в листинге 3.7.
118 Глава 3. Контракты данных Листинг 3-7- Отсутствующие поля инициализируются значениями по умолчанию ///////////////// Сторона службы ///////////////// [DataContract] struct Contact { [DataMember] public string FirstName; [DataMember] public string LastName; [DataMember] public string Address: } [ServiceContract] Interface IContactManager { [Operationcontract] void AddContact(Contact contact); ) class ContactManager : IContactManager { public void AddContact(Contact contact) { Trace.WrlteLlneC'First name - ” + contact.FlrstName); Trace.WrlteLlneCLast name = " + contact.LastName); Trace.WrlteLlneC'Address - " + (contact.Address ?? "Missing")): } } ///////////////// Сторона клиента ///////////////// [DataContract] struct Contact { [DataMember] public string FirstName; [DataMember] public string LastName; } Contact contact = new ContactO; contact.FirstName - "Juwal"; contact.LastName « "Lowy"; ContactManagerCllent proxy - new ContactManagerCl1ent(); proxy.AddContact(contact); proxy.Close();
Изменение версий 119 Результат выполнения этого кода выглядит так: First name = Juwal Last name - Lowy Address • Missing Это объясняется тем, что служба получила null в качестве значения поля Address и вывела в трассировке строку Missing. Использование события OnDeserializing Событие OnDeserializing может использоваться для инициализации отсутствую- щих полей данных на основании некой локальной эвристики. Если сообщение содержит данные, они заменят величины, заданные в событии OnDeserializing, а если нет — полю будет присвоено значение, отличное от значения по умолча- нию: [DataContract] struct Contact [DataMember] public string FirstName; [DataMember] public string LastName: [DataMember] public string Address; [OnDeserializing] void OnDeserializing(StreamingContext context) ( Address - "Some default address"; ) } В этом случае результат выполнения листинга 3.7 будет выглядеть так: First name * Juwal Last name • Lowy Address “ Some default address Обязательные поля В отличие от игнорирования новых полей, которое в большинстве случаев без- вредно, стандартная обработка отсутствующих полей с большой вероятностью вызовет сбой в цепочке вызовов на принимающей стороне, потому что отсутст- вующие поля могут быть необходимы для правильного функционирования. Это может привести к катастрофическим последствиям. Задавая свойство IsRe- quired атрибута DatMember равным true, вы приказываете WCF отказаться от вы- зова операции при отсутствии полей данных: [DataContract] struct Contact [DataMember] public string FirstName;
120 Глава 3. Контракты данных [DataMember] public string LastName; [DataMember(IsRequired - true)] public string Address; } По умолчанию свойство IsRequired равно false, то есть отсутствующие поля игнорируются. Если DataContractSerializer на принимающей стороне не находит информацию, необходимую для десериализации поля, помеченного в сообще- нии как обязательное, вызов отменяется с выдачей исключения NetDispatcher- FaultException на отправляющей стороне. Если бы в контракте данных на сторо- не службы в листинге 3.7 поле Address было помечено как обязательное, то вызов не достиг бы службы. «Обязательность» поля публикуется в метаданных службы, а при импортировании на сторону клиента в сгенерированном файле с определением посредника будет включено правильное значение этого свойства. Как клиент, так и служба могут пометить часть своих полей данных (или все поля) как обязательные, полностью независимо друг от друга. Чем больше по- лей помечено как обязательные, тем надежнее взаимодействие со службой или клиентом, но за счет гибкости и возможностей изменения версий. Когда контракт данных с обязательным новым полем отправляется прини- мающей стороне, не подозревающей о существовании такого поля, такой вызов вполне допустим. Иначе говоря, даже если свойство IsRequired задается равным true для нового поля контракта данных версии 2 (V2), V2 можно отправить сто- роне, которая ожидает получить контракт версии 1 (VI), не содержащий такого поля. Новое поле будет попросту проигнорировано. Свойство IsRequired дейст- вует только при отсутствии поля на стороне, осведомленной о существова- нии V2. Допустим, VI не знает о добавлении нового поля в V2; в табл. 3.1 приве- дены возможные комбинации разрешенных или запрещенных взаимодействий в зависимости от задействованных версий и значения свойства IsRequired. Таблица 3.1. Взаимодействие контрактов данных с обязательными полями IsRequired От VI к V2 От V2 к VI false Да Да true Нет Да Одним из интересных примеров применения обязательных полей являются сериализуемые типы. Поскольку сериализуемые типы по умолчанию не допус- кают отсутствие полей, при их экспортировании в итоговом контракте данных все поля данных помечаются как обязательные. Например, у следующего опре- деления Contact: [Serializable] struct Contact { public string FirstName; public string LastName; }
Изменение версий 121 представление метаданных будет выглядеть так: [DataContract] struct Contact [DataMember(IsRequired - true)] public string FirstName {get;set;} [DataMember(IsRequired « true)] public string LastName {get:set:} Чтобы пометить поле как необязательное, примените к нему атрибут OptionalField. Например, у следующего определения Contact: [Serializable] struct Contact public string FirstName; [OptionalField] public string LastName: ) представление метаданных принимает вид [DataContract] struct Contact { [DataMember (I sRequi red = true)] public string FirstName {get;set:} [DataMember] public string LastName {get:set:} I Передача с промежуточным изменением версии Методы обеспечения совместимости, рассматривавшиеся до настоящего мо- мента (игнорирование новых полей и задание значений по умолчанию для от- сутствующих полей), не оптимальны. Они делают возможными точечные взаи- модействия между клиентом и службой, но не поддерживают более широкого сценария сквозной передачи. Рассмотрим два примера взаимодействия, пока- занных на рис. 3.4. В первом взаимодействии клиент, построенный на базе нового контракта данных с новыми полями, передает этот контракт службе А, которой о новых полях ничего не известно. Затем служба А передает данные службе В, поддер- живающей новый контракт. Однако данные, передаваемые от А к В, не содер- жат новых полей — эти поля были потеряны при десериализации, потому что они не входят в контракт данных службы А. Вторая ситуация выглядит анало- гично: клиент, поддерживающий новый контракт данных с новыми полями,
122 Глава 3. Контракты данных передает данные службе С; службе известен только старый контракт, не содер- жащий новых полей. В данных, возвращаемых службой С клиенту, новые поля отсутствуют. Рис. 3.4. Передача с промежуточным изменением версии может привести к частичной потере данных Подобные взаимодействия вида «новый-старый-новый» называются переда- чами с промежуточным изменением версии. В WCF предусмотрена возмож- ность их обработки. Служба (или клиент) с поддержкой старого контракта про- сто передает данные состояния новых полей без их потери. Проблема лишь в том, как выполнить сериализацию и десериализацию неизвестных полей при отсутствии схемы и где хранить их между вызовами. Для решения проблемы тип контракта данных реализует интерфейс lExtensibleDataObject, определяемый следующим образом: public Interface lExtensibleDataObject { Extensi onDataObject Extens i onData {get;set:} } lExtensibleDataObject определяет единственное свойство типа Extension Data- Object. Точное определение ExtensionDataObject несущественно, потому что раз- работчику никогда не приходится работать с ним напрямую. ExtensionDataObject представляет собой внутренний связанный список, содержащий ссылки на object и информацию о типах; именно здесь сохраняются неизвестные поля дан- ных. Если тип контракта данных поддерживает lExtensibleDataObject, то неопо- знанные новые поля, обнаруженные в сообщении, десериализуются и сохраня- ются в этом списке. Когда служба (или клиент) передает при вызове тип старого контракта данных, который теперь содержит неизвестные поля данных в ExtensionDataObject, неизвестные поля сериализуются в сообщение. Если при- нимающая сторона знает о существовании нового контракта данных, она полу- чает полный набор данных без отсутствующих полей. Реализация и использо- вание lExtensibleDataObject продемонстрированы в листинге 3.8. Как видите, реализация весьма прямолинейна — в класс просто добавляется свойство Exten- sionDataObject для чтения/записи соответствующей переменной.
Изменение версий 123 Листинг 3.8. Реализация JExtensibleDataObject [DataContract] class Contact : lExtensibleDataObject ( ExtenslonDataObject m_ExtensionData: public ExtenslonDataObject ExtensionData { get { return mExtensionData: } set { m_ExtensionData - value: } } [DataMember] public string FirstName; [DataMember] public string LastName; ) Совместимость схем Хотя реализация lExtensibleDataObject делает возможной передачу с промежу- точным изменением версии, у нее имеется и обратная сторона: служба, совмес- тимая с одной схемой контракта данных, способна успешно взаимодействовать с другой службой, рассчитанной на другую схему контракта данных. В некото- рых специфических случаях служба может принять решение о запрете переда- чи с промежуточным изменением версии и потребовать строгого соблюдения своей версии контракта данных в нисходящих службах. Служба приказывает WCF переопределить механизм обработки неизвестных полей в lExtensibleData- Object и игнорировать их даже в том случае, если контракт данных поддержива- ет lExtensibleDataObject. Задача решается посредством использования атрибута ServiceBehavior. Аспекты поведения и этот атрибут будут подробно описаны в следующей главе, а пока достаточно сказать, что атрибут ServiceBehavior содер- жит логическое свойство IgnoreExtensionDataObject, определяемое следующим образом: [Attri butells age (Attri buteTargets .Class)] public sealed class ServIceBehaviorAttribute ; Attribute.... public bool IgnoreExtensionDataObject (get;set;} // ... I По умолчанию свойство IgnoreExtensionDataObject равно false. Если задать его равным true, все неизвестные поля данных во всех контрактах данных, исполь- зуемых службой, всегда будут игнорироваться:
124 Глава 3. Контракты данных [ServiceBehaviordgnoreExtensionDataObject - true)] class ContactManager : IContactManager {•••} Если контракт данных импортируется посредством SvcUtil или Visual Stu- dio, сгенерированный тип контракта данных всегда поддерживает lExtensible- DataObject даже если в исходном контракте данных этот интерфейс не поддер- живался. Полагаю, в своих контрактах данных вам всегда следует реализовывать lExtensibleDataObject, и по возможности избегать установки IgnoreExtensionData- Object. Интерфейс lExtensibleDataObject способствует изоляции службы от нис- ходящих служб, давая возможность ей развиваться отдельно от них. ПРИМЕЧАНИЕ----------------------------------------------------------------------------- При работе с известными типами реализация lExtensibleDataObject не обязательна, потому что суб- класс всегда десериализуется без потери данных. Перечисления Перечисления всегда сериализуемы по определению. При определении нового перечисления применять к нему атрибут DataContract не обязательно, и оно мо- жет свободно использоваться в контракте данных, как показано в листинге 3.9. Все значения, входящие в перечисление, неявно включаются в контракт дан- ных. Листинг 3.9. Перечисление в контакте данных enum ContactType { Customer. Vendor. Partner } [DataContract] struct Contact { [DataMember] public ContactType ContactType; [DataMember] public string FirstName; [DataMember] public string LastName; } Если некоторые элементы перечисления должны быть исключены из кон- тракта данных, перечисление необходимо снабдить атрибутом DataContract. Да- лее все элементы, которые должны войти в контракт, явно помечаются атрибу- том EnumMemberAttribute. Атрибут EnumMember определяется так: [AttributellsageCAttributeTargets.Field.Inherited - false)] public sealed class EnumMemberAttribute ; Attribute
Перечисления 125 public string Value (get;set:} ) Любой элемент перечисления, не помеченный атрибутом EnumMember, не вой- дет в контракт данных. Например, для перечисления [DataContract] enum ContactType ( [EnumMember] Customer, [EnumMember] Vendor. // He войдет в контракт данных Partner ) канальное представление будет выглядеть так: enum ContactType ( Customer. Vendor I Атрибут EnumMember также применяется для назначения псевдонимов неко- торых элементов перечисления в соответствии с уже существующим контрак- том данных; для этой цели используется свойство Value. Например, для пере- числения [DataContract] enum ContactType ( [EnumMember (Value = "MyCustomer")] Customer. [EnumMember] Vendor. // He войдет в контракт данных Partner ) будет создано следующее канальное представление: enum ContactType ( MyCustomer. Vendor. Partner I Действие атрибута EnumMember локально по отношению к той стороне, кото- рая его использует. При публикации метаданных (или при определении их на стороне клиента) в итоговом контракте данных этот атрибут не встречается, и используется только окончательный результат.
126 Глава 3. Контракты данных Делегаты и контракты данных Все определения делегатов компилируются в сериализуемые классы, поэтому теоретически делегаты могут входить в типы контрактов данных в виде пере- менных: [DataContract] class MyDataContract { [DataMember] public EventHandler MyEvent; } и даже событий (обратите внимание на квалификатор field): [DataContract] class MyDataContract { [f1eld:DataMember] public event EventHandler MyEvent; } На практике импортированный контракт будет содержать недействительное определение делегата. Хотя определение можно подправить вручную, более серьезная проблема заключается в том, что при сериализации объекта с пере- менной-делегатом также будет сериализован внутренний список вызова делега- тов. Как правило, для служб и клиентов такой эффект нежелателен, потому что точная структура списка является локальной для клиента или службы и не должна выходить за границу службы. Кроме того, нет гарантий, что объекты во внутреннем списке сериализуемы или имеют допустимые контракты данных. Соответственно, в одних случаях сериализация сработает, в других — нет. Самое простое решение проблемы — не применять атрибут DataMember к де- легатам. Если контракт данных является сериализуемым типом, делегата сле- дует явно исключить из него: [Serializable] public class MyClass { [NonSeri all zed] EventHandler m_MyEvent: } Наборы данных и таблицы Одну из самых распространенных категорий контрактов данных, передаваемых между клиентами и службами, составляют данные, полученные из базы данных (или предназначенные для сохранения в ней). В .NET взаимодействия с базами данных традиционно осуществляются через типы наборов и таблиц данных ADO.NET. Приложения могут работать с базовыми типами DataSet и DataTable или же сгенерировать типизированные субклассы средствами доступа к дан- ным VisuaX Studio.
Наборы данных и таблицы 127 Базовые типы DataSet и DataTable сериализуемы и помечены атрибутом Serializable: [Serializable] public class DataSet : ... (...) [Serializable] public class DataTable : ... (...) Следовательно, мы можем определять допустимые контракты служб, кото- рые принимают и возвращают таблицы и наборы данных: [DataContract] struct Contact (...) [DataContract] interface IContactManager ( [Operationcontract] void AddContact(Contact contact): [Operationcontract] void AddContacts(DataTable contacts): [Operationcontract] DataTable GetContactsO; При импортировании определения этого контракта службы сгенерирован- ный файл посредника будет содержать определение контракта данных Data- Table - только схему DataTable, без какого-либо кода. Вы можете удалить это оп- ределение из файла и воспользоваться ADO.NET. Также в контрактах можно использовать типизированные субклассы DataSet и DataTable. Допустим, имеется таблица базы данных с именем ContactsDataTable, в которой присутствуют столбцы FirstName, LastName и т. д. Visual Studio может построить класс набора данных MyDataSet, который содержит вложенный класс ContactsDataTable, класс записи и класс адаптера данных (листинг 3.10). Листинг 3.10. Типизированные классы набора и таблицы данных [Serializable] public partial class MyDataSet : DataSet ( public ContactsDataTable Contacts (get:) [Serializable] public partial class ContactsDataTable : DataTable. lEnumerable ( public void AddContactsRow(ContactsRow row); public ContactsRow AddContactsRow(string FirstName.string LastName): // ... I
128 Глава 3. Контракты данных public partial class ContactsRow : DataRow { public string FirstName {get;set;} public string LastName {get;set;} // } // } public partial class ContactsTableAdapter : Component { public virtual MyDataSet.ContactsDataTable GetDataO; // } Типизованная таблица данных может использоваться в контракте службы: [DataContract] struct Contact {...} [ServiceContract] interface IContactManager { [Operationcontract] void AddContact(Contact contact); [Operationcontract] void AddContacts(MyDataSet.ContactsDataTable contacts); [Operationcontract] MyDataSet.ContactsDataTable GetContacts(); } ВНИМАНИЕ ---------------------------------------------------------------------------------- Класс записи данных не сериализуем, поэтому он (а также его типизованные субклассы) не может использоваться в операциях: // Недопустимое определение [Operationcontract] void AddContact(MyDataSet.ContactRow contact); Типизованная таблица данных является частью публикуемых метаданных службы. При импортировании на стороне клиента SvcUtil и Visual Studio вос- создают типизованную таблицу данных, а файл посредника включает не только контракт данных, но и сам код. Если у клиента уже имеется локальное опреде- ление типизованной таблицы, определение можно исключить из файла посред- ника. Массивы вместо таблиц Благодаря инструментарию ADO.NET и Visual Studio использование DataSet и Data Table (и их типизованных субклассов) в клиентах и службах WCF стано-
Наборы данных и таблицы 129 вится делом тривиальным. Правда, эти типы доступа к данным относятся к спе- цифике .NET. Хотя они сериализуемы, итоговая схема контракта данных полу- чается настолько сложной, что попытки организовать взаимодействие с ней на других платформах нельзя считать практичным решением. Использование таб- лиц и наборов данных имеет и другие недостатки: в частности, оно способству- ет раскрытию внутренней структуры данных, а будущие изменения в схеме базы данных могут отразиться на клиентах. Если передача таблицы данных в границах приложения может быть приемлема, выход таблицы данных за гра- ницы приложения или общедоступной службы вряд ли можно посчитать удач- ным решением. В общем случае доступ следует предоставлять к операциям с данными, а не к самим данным. Если же возникнет необходимость в передаче самих данных, воспользуйтесь нейтральной структурой данных — например, массивом. Для упрощения задачи преобразования таблицы данных в массив вы можете воспользоваться моим классом DataTableHelper, который определяется следующим образом: public static class DataTableHelper ( public static T[] ToArray<R.T>(DataTable table. Converters, T>. converter) where R : DataRow; Для работы DataTableHelper необходим лишь преобразователь записи данных таблицы в контракт данных. Кроме того, DataTableHelper обеспечивает частич- ную проверку типов на стадии компиляции и выполнения. Пример использова- ния DataTableHelper представлен в листинге 3.11. Листинг 3.11. Использование DataTableHelper [DataContract] struct Contact I [DataMember] public string FirstName: [DataMember] public string LastName: ) [ServiceContract] interface IContactManager I [Operationcontract] Contacts GetContactsO: class ContactManager : IContactManager ( public Contactf] GetContactsO ( ContactsTableAdapter adapter = new ContactsTableAdapter(): MyDataSet.ContactsDataTable contacsTable = adapter,GetData(): Converter<MyDataSet.ContactsRow.Contact> converter: Converter = delegate(MyDataSet.ContactsRow row) продолжение &
130 Глава 3. Контракты данных { Contact contact - new ContactO; contact.FirstName row.FirstName; contact.LastName - row.LastName; return contact: }; return DataTableHelper .ToAr ray (contactsTable,converter); } // } В листинге 3.11 метод GetContactsO использует типизованный адаптер табли- цы ContactsTableAdapter (см. листинг 3.10) для получения записей из базы дан- ных в форме типизованной таблицы MyDataSet.ContactsDataTable. Затем GetCon- tactsO определяет анонимный метод, преобразующий экземпляр типизованной записи данных MyDataSet.ContactsRow в экземпляр Contact. Далее GetContactsO вызывает метод DataTableHapler.ToArray(), передавая ему таблицу и преобразова- тель. Реализация DataTableHelper.ToArray() приведена в листинге 3.12. Листинг 3.12. Класс DataTableHelper public static class DataTableHelper { public static T[] ToArray<R.T>(DataTable table,Converters, T> converter) where R : DataRow { if(table.Rows.Count == 0) { return new T[]{}; } // Проверить T на [DataContract] или [Serializable] Debug.Assert(IsDataContract(typeof(T)) || typeof(T).IsSerializable); // Убедиться в том, что таблица содержит правильные записи Debug.Assert(MatchingTableRow<R>(table)): return Col 1ection.UnsafeToArray(table.Rows.converter); } static bool IsDataContracttType type) { object[] attributes = type.GetCustomAttributes(typeof(DataContractAttribute),false); return attributes.Length == 1; } static bool MatchingTableRow<R>(DataTable table) { if(table.Rows.Count — 0) { return true: } return table.Rows[0] is R; } }
Обобщенные типы 131 В сущности, метод DataTableHelper.ToArray() всего лишь вызывает преобразо- ватель для каждой записи в таблице при помощи вспомогательного класса myCollection. Каждая запись преобразуется в объект Contact, а полученный мас- сив возвращается методом. DataTableHelper также обеспечивает некоторый уро- вень безопасности типов. Во время компиляции для «параметра типа» R он убе- ждается в том, что этот тип представляет запись данных. На стадии выполнения метод ТоАггау() для пустой таблицы возвращает пустой массив. Метод также проверяет, что параметр типа Т помечен атрибутом DataContract или Serializable. Проверка атрибута DataContract осуществляется при помощи вспомогательного метода IsDataContract(), использующего рефлексию для поиска атрибута. Для атрибута Serializable метод проверяет, установлен ли для типа флаг IsSerializable. Напоследок ТоАггау() проверяет, содержит ли предоставленная таблица записи, определяемые параметром типа R. Для этого используется вспомогательный ме- тод MatchingTableRow(), который получает первую запись и проверяет ее тип. Обобщенные типы Не допускается определение контрактов WCF, зависящих от обобщенных пара- метров типа. Обобщенные типы (generics) относятся к специфике .NET, и их передача нарушила бы служебно-ориентированную природу .NET. Впрочем, использование ограниченных обобщенных типов (bounded generic types) в кон- трактах данных возможно — при условии, что в контракте службы будут указа- ны параметры типов, обладающие допустимыми контрактами данных, как по- казано в листинге 3.13. Листинг 3.13. Использование ограниченных обобщенных типов [DataContract] class MyClass<T> [DataMember] public T m_MyMember; [ServiceContract] interface IMyContract [Operationcontract] void MyMethod(MyClass<int> obj); ) При импортировании метаданных контракта из листинга 3.13 все параметры типов в импортированных типах заменяются конкретными типами, а контракт данных переименовывается по схеме исходное имя>О^<имена параметров типов><хеш> Для определений из листинга 3.13 импортированный контракт данных и контракт службы будут выглядеть так: [DataContract] class MyCl assOflnt
132 Глава 3. Контракты данных ( int MyMemberField; [DataMember] public int m_MyMember { get ( return MyMemberField; } set { MyMemberField = value; } } } [ServiceContract] interface IMyContract { [Operationcontract] void MyMethodCMyClassOflnt obj): } Если бы вместо int в контракте службы использовался пользовательский тип SomeClass: [DataContract] class SomeClass {•••} [DataContract] class MyClass<T> {...} [Operationcontract] void MyMethod(MyClass<SomeClass> obj); то экспортированный контракт данных мог бы выглядеть примерно так: [DataContract] class SomeClass {...} [DataContract] class MyClass0fSomeClassMTRdqN6P {...} [Operationcontract] void MyMethod(MyClass0fSomeClassMTRdqN6P obj); Здесь MTRdqN6P — некий квазиуникальный хеш обобщенного параметра типа и вмещающего пространства имен. Для разных контрактов данных и про- странств имен генерируются разные хеши. Присутствие хеша снижает вероят- ность конфликта с другим контрактом данных, использующим другой параметр типа с тем же именем. При использовании примитивов в качестве обобщенных параметров типов хеш не генерируется.
Обобщенные типы 133 В большинстве случаев хеш оказывается лишней — и притом громоздкой — предосторожностью. Чтобы переименовать экспортированный контракт дан- ных, достаточно задать другое имя свойству Name контракта данных. Например, для следующего контракта данных на стороне службы: [DataContract] class SomeClass (•••} [DataContract(Name - "MyClass")] class MyClass<T> (...) [Operationcontract] void MyMethod(MyClass<SomeClass> obj): экспортированный контракт будет выглядеть так: [DataContract] class SomeClass (...) [DataContract] class MyClass (...) [Operationcontract] void MyMethodtMyClass obj); Впрочем, если вы хотите использовать схему с объединением имени обоб- щенного параметра типа и имени контракта данных, используйте директиву {<номер>}, где номер — порядковый номер параметра типа. Например, для сле- дующего определения на стороне службы: [DataContract] class SomeClass (...) [DataContract (Name = ”MyClassOf{O}{l}")] class MyClass<T.U> (...) [Operationcontract] void MyMethod(MyClass<SomeClass.int> obj); будет получено следующее экспортированное определение: [DataContract] class SomeClass (...) [DataContract] class MyClassOfSomeClassint (•••) [OperationContract(...)] void MyMethod(MyClassOfSomeClass1nt obj);
134 Глава 3. Контракты данных ВНИМАНИЕ ------------------------------------------------------------- Количество параметров типа не проверяется на стадии компиляции. Несовпадение приведет к ис- ключению времени выполнения. Наконец, присоединение # после номера генерирует уникальный хеш. На- пример, для следующего определения контракта данных: [DataContract] class SomeClass [DataContract(Name = "MyClassOf{0}{#}")] class MyClass<T> [Operationcontract] void MyMethod(MyClass<SomeClass> obj): будет получено следующее экспортированное определение: [DataContract] class SomeClass [DataContract] class MyClass0fSomeClassMTRdqN6P {••} [0perat1onContract(...)] void MyMethod(MyClass0fSomeClassMTRdqN6P obj); Коллекции В .NET коллекцией называется любой тип, поддерживающий интерфейсы IEnu- merable или IEnumerable<T>. Все встроенные коллекции .NET — массивы, списки и стеки — поддерживают эти интерфейсы. Контракт данных может включать коллекцию как поле данных, или контракт службы может определять опера- цию, которая взаимодействует с коллекцией напрямую. Поскольку коллекции .NET относятся к специфике .NET, WCF не может напрямую использовать их в метаданных службы. Тем не менее коллекции чрезвычайно полезны, поэтому в WCF были определены специальные правила их маршалинга. При определении операции службы, использующей любой из следующих интерфейсов коллекций: IEnumerable<T>, IList<T> и ICoLLection<T>, в канальном представлении всегда будет задействован массив. Например, следующее опре- деление контракта службы и реализация: [ServiceContract] Interface IContactManager { [Operationcontract] IEnumerable<Contact> GetContacts();
class ContactManager : IContactManager List<Contact> m_Contacts = new L1st<Contact>(); public lEnumerable<Contact> GetContactsO ( return m_Contacts: ) ) экспортируются в следующем виде: [ServiceContract] interface IContactManager ( [Operationcontract] Contactt] GetContactsO; ) Реальные коллекции Если коллекция, включенная в контракт, является реальной (concrete)(B отли- чие от интерфейсов) и сериализуемой (то есть помеченной атрибутом Seria- lizable, но не атрибутом DataContract), WCF может автоматически нормализо- вать коллекцию в массив с соответствующим типом элементов. Для этого коллекция должна содержать метод Add() с одной из следующих сигнатур: public void Add (object obj); // Коллекция использует lEnumerable public void Add(T item): // Коллекция использует lEnumerable<T> Например, возьмем следующее определение контракта: [ServiceContract] interface IContactManager ( [Operationcontract] void AddContact(Contact contact); [Operationcontract] L1st<Contact> GetContactsO: } Класс списка определяется следующим образом: public interface ICol 1 ection<T> : lEnumerable<T> (...) public interface IList<T> : ICollection<T> (••) [Serializable] public class List<T> : IList<T> ( public void Add(T item); //... ) Коллекция является допустимой и содержит метод Add(), поэтому итоговое канальное представление контракта будет выглядеть так:
136 Глава 3. Контракты данных [ServiceContract] Interface IContactManager { [Operationcontract] void AddContact(Contact contact): [Operationcontract] Contacts GetContactsO: } Итак, List<Contacts> маршализируется как Contact[]. При этом служба может возвращать List<Contacts>, но клиент будет взаимодействовать с массивом, как показано в листинге 3.14. Листинг 3.14. Маршалинг списка как массива ///////////////// Сторона службы ///////////////// [ServiceContract] Interface IContactManager { [Operationcontract] void AddContact(Contact contact): [Operationcontract] List<Contact> GetContactsO: } // Реализация службы class ContactManager : IContactManager { L1st<Contact> m_Contacts = new L1st<Contact>(): public void AddContact(Contact contact) { m_Contacts.Add(contact); } public List<Contact> GetContactsO { return m_Contacts; } } ///////////////// Сторона клиента ///////////////// [ServiceContract] Interface IContactManager { [Operationcontract] void AddContact(Contact contact); [Operationcontract] Contact[] GetContactsO: } public partial class ContactManagerCllent : C11entBase<IContactManager>. IContactManager { public Contact[] GetContactsO
return Channel .GetContactsO; ) II Клиентский код ContactManagerCllent proxy = new ContactManagerClientO; Contact[] contacts = proxy.GetContactsO; proxy.Close(); Обратите внимание: коллекция должна содержать метод Add() для обеспече- ния маршалинга в виде массива; при этом наличие реализации Add() не обяза- тельно. Пользовательские коллекции Возможность автоматического маршалинга коллекции в виде массива не огра- ничивается встроенными коллекциями. Подойдет любая пользовательская кол- лекция, удовлетворяющая тем же исходным условиям (листинг 3.15). В приве- денном примере коллекция MyCollection<string> маршализируется в виде string[]. Листинг 3.15. Маршалинг пользовательской коллекции в виде массива III////////////// Сторона службы ///////////////// [Serializable] public class MyCol 1 ection<T> : IEnumerable<T> public void Add(T item) {} IEnumerator<T> I Enumerable<T>. Get Enumerator) (...) // ... ) [ServiceContract] interface IMyContract [Operationcontract] MyCollection<string> GetCollectionO: ) lllllllllllllllll Сторона клиента ///////////////// [ServiceContract] interface IMyContract [Operationcontract] string[] GetCollectionO; ) Контракт данных коллекций Описанный механизм маршалинга реальных коллекций не оптимален. Во-пер- вых, он требует, чтобы коллекция была сериализуемой, и не работает со служеб- но-ориентированным атрибутом DataContract. Одна сторона имеет дело с коллек- цией, а другая — с массивом. Эти две сущности семантически не эквивалентны —
138 Глава 3. Контракты данных скорее всего, коллекция обладает некоторыми преимуществами, иначе бы она изначально не использовалась. Во-вторых, поддержка метода Add() или интер- фейсов IEnumerable/IEnumerable<T> не проверяется на стадии компиляции или времени выполнения; их отсутствие приводит к неработоспособности контрак- та данных. Проблема решается при помощи очередного специализированного атрибута CollectionDataContractAttribute, определяемого следующим образом: [Attri butellsage(AttributeTargets.Struct|Attri buteTargets.Class,Inheri ted=fal se) ] public sealed class CollectionDataContractAttribute : Attribute { public string Name {get;set;} public string Namespace {get;set:} //... } Атрибут CollectionDataContractAttribute аналогичен DataContract, и он не делает коллекцию сериализуемой. Коллекция, к которой он применяется, предостав- ляется клиенту как обобщенный связанный список. Вполне возможно, что свя- занный список не имеет ничего общего с исходной коллекций, но по своему ин- терфейсу он все же больше напоминает коллекцию, чем массив. Например, для следующего определения коллекции: [CollectionDataContract(Name = "MyCol1ectionOf{0}")] public class MyCollection<T> : lEnumerable<T> { public void Add(T Item) 0 IEnumerator<T> IEnumerable<T>.GetEnumerator() // ... } и определения контракта на стороне службы: [ServiceContract] interface IContactManager { [Operationcontract] void AddContact(Contact contact); [Operationcontract] MyCol1ection<Contact> GetContactsO; } после импортирования метаданных клиент получит следующие определения: [CollectionDataContract] public class MyCol1ectionOfContact : LIst<Contact> {} [ServiceContract] interface IContactManager { [Operationcontract] void AddContact(Contact contact);
[Operationcontract] MyCollectionOfContact GetContactsO: Кроме того, атрибут CollectionDataContract проверяет во время загрузки служ- бы присутствие метода Add(), а также интерфейса innumerable или IEnumerable<T>. При отсутствии их в коллекции происходит исключение InvalidDataContractEx- ception. Атрибуты DataContract и CollectionDataContract не могут применяться к кол- лекции одновременно. Это условие также проверяется во время загрузки службы. Сохранение типа коллекции WCF даже позволяет использовать на стороне клиента ту же коллекцию, что и на стороне службы. У программы SvcUtil имеется ключ /collectionType (сокра- щенно /ct), который позволяет сослаться на конкретную сборку коллекции на стороне клиента и использовать ее в определении контракта. Вы должны задать местонахождение сборки коллекции, и разумеется, сборка должна быть доступ- на для клиента. Например, служба может определить следующий контракт с использовани- ем коллекции Stack<T>: [ServiceContract] interface IContactManager [Operationcontract] void AddContact(Contact contact); [Operationcontract] Stack<Contact> GetContactsO: ) Клиент передает SvcUtil адрес обмена метаданными службы (например, http://localhost:8000), использует ключ /г для ссылки на сборку System.dll, содер- жащую класс Stack<T>, и при помощи ключа /ct указывает на необходимость со- хранения исходной коллекции: SvcUtil http://1 ocalhost:8000/ /г: С: \WINDOWS\Mi crosoft.NET\Framework\v2.0.50727\System.dll /ct: System. Col 1 ect 1 ons. Gener 1 c. Stack41 В итоговом определении контракта на стороне клиента будет использовать- ся Stack<T>: [ServiceContract] interface IContactManager [Operationcontract] void AddContact(Contact contact): [Operationcontract] Stack<Contact> GetContactsO:
140 Глава 3. Контракты данных Разумеется, решение с ключами /ret нельзя назвать служебно-ориентирован- ным. Тип используемой коллекции должен быть известен заранее, к тому же та- кое решение работает только во взаимодействиях WCF-WCF. Коллекции на стороне клиента До настоящего момента работа с коллекциями обсуждалась в определенном контексте: коллекция определяется и используется на стороне службы, а кли- ент взаимодействует с массивом или списком. Оказывается, возможен и обрат- ный вариант: служба определяется в виде массива, а клиент использует совмес- тимую коллекцию. Например, возьмем следующее определение контракта службы: [ServiceContract] interface IContactManager { [Operationcontract] void ProcessArray(string[] array); } По умолчанию импортированные определения службы и посредника будут идентичными. Однако клиент может вручную переработать контракт и посред- ника для использования интерфейса любой коллекции: // Переработанное определение: [ServiceContract] interface IContactManager { [Operationcontract] void ProcessArray(IIIst<string> list); } и передать при вызове коллекцию с методом Add() (сериализуемую или снаб- женную атрибутом CollectionDataContract): IList<string> list = new List<string>; MyContactClient.proxy = new MyContractClient(); proxy. ProcessArrayd 1st); proxy.CloseO; Итераторы C# При использовании механизма итераторов1 C# 2.0 можно поручить компилято- ру сгенерировать реализацию пользовательского итератора для коллекции. Тем не менее такая реализация оформляется в виде вложенного класса, не помечен- ного атрибутом Serializable. Соответственно, коллекция не может возвращаться напрямую методом службы: [ServiceContract] interface IContactManager { [Operationcontract] 1 Если вы незнакомы с итераторами C# 2.0, обратитесь к моей статье «Create Elegant Code with Ano- nymous Methods, Iterators, and Partial Classes» в «MSDN Magazine» за май 2004 г.
IEnumerable<Contact> GetContactsO; I class ContactManager : IContactManager I List<Contact> m_Contacts = new List<Contact>(); // Недопустимая реализация public IEnumerable<Contact> GetContactsO ( foreach(Contact contact in m_Contacts) ( yield return contact; } ) ) ПРИМЕЧАНИЕ---------------------------------------------------------------------------------- В следующей версии C# (а также в остальных компонентах .NET 3.5) компилятор будет добавлять атрибут Serializable во вложенный класс, сгенерированный командой yield return. Это сделает воз- можным прямой возврат итератора методами служб. Словари Словари (ассоциативные массивы) составляют особую разновидность коллек- ций, в которой один тип контракта данных отображается на другой тип. Как следствие, словари не слишком хорошо укладываются в модель массива или списка, и в WCF они получили собственное представление. Если словарь является сериализуемой коллекцией с поддержкой интерфей- са IDictionary, он представляется в виде Dictionary<objectobject>. Например, оп- ределение контракта службы [Serializable] public class MyDictionary : IDictionary {•••} [ServiceContract] interface IContactManager I [Operationcontract] MyDictionary GetContactsO; } будет представлено для внешнего доступа следующим определением: [ServiceContract] interface IContactManager [Operationcontract] Dictionary<object,object> GetContactsO; В частности, это относится к коллекции HashTable.
142 Глава 3. Контракты данных Для сериализуемых коллекций с поддержкой интерфейса IDictionary<KrT>: [Serializable] public class MyDictionary<K,T> ; IDictionary<K,T> {•••} [ServiceContract] interface IContactManager { [Operationcontract] MyDicti onary<i nt,Contact> GetContacts(); } используется канальное представление Dictionary<K,T>: [ServiceContract] interface IContactManager { [Operationcontract] Dictionary^ nt,Contact» GetContactsO; } Сюда относится и прямое использование Dictionary<K,T> в исходном опреде- лении. Если вместо обычной сериализуемой коллекции используется словарь, по- меченный атрибутом CollectionDataContract, он будет представлен субклассом со- ответствующего представления. Например, следующее определение контракта службы: [CollectionDataContract] public class MyDictionary : IDictionary [ServiceContract] interface IContactManager { [Operationcontract] MyDictionary GetContactsO; } будет обладать канальным представлением: [CollectionDataContract] public class MyDictionary : Dictionary<object,object» {} [ServiceContract] interface IContactManager { [Operationcontract] MyDictionary GetContactsO;
Коллекции 143 А обобщенная коллекция [CollectionDataContract] public class MyDictionary<K,T> : IDictionary<K,T> () [ServiceContract] interface IContactManager ( [Operationcontract] MyDictionary<int,Contact> GetContactsO: публикуется в метаданных в следующем виде: [Col 1 ect i onDa taCont ract ] public class MyDictionary : D1ct1onary<1nt,Contact> () [ServiceContract] interface IContactManager ( [Operationcontract] MyDictionary GetContactsO;
4 Управление экземплярами Термином «управление экземплярами» (instance management) обозначается со- вокупность приемов, используемых WCF для привязки клиентских запросов к экземплярам служб и определяющих, какой экземпляр службы будет обраба- тывать запрос клиента. Необходимость управления экземплярами объясняется тем, что приложения слишком сильно различаются по своим потребностям в области масштабируемости, производительности, пропускной способности, обработки транзакций и очередей вызовов. В том, что касается этих потребно- стей, единого решения «на все случаи жизни» просто не существует. Тем не менее существует ряд канонических стратегий управления экземплярами, при- менимых для широкого спектра приложений, разнообразных сценариев и про- граммных моделей. Эти стратегии будут рассмотрены в настоящей главе, а их понимание необходимо для разработки масштабируемых, логически целостных служебно-ориентированных приложений. WCF поддерживает три типа активи- зации экземпляров: у служб уровня вызова для каждого клиентского запроса создается (и впоследствии уничтожается) новый экземпляр службы. У служб уровня сеанса (или сеансовых служб) создается один экземпляр службы для ка- ждого подключения клиента. Наконец, у синглетных служб все подключения клиентов обслуживаются одним экземпляром. В этой главе описаны все режи- мы управления экземплярами; приводятся рекомендации относительно того, когда и как лучше их использовать; а также рассматриваются некоторые сопут- ствующие темы: аспекты поведения, контексты, демаркационные операции и деактивизация экземпляров1. Аспекты поведения В общем и целом выбор режима управления экземплярами является подробно- стью реализации на стороне службы, которая не должна как-либо проявляться 1 Глава содержит выдержки из моей статьи «WCF Essentials: Discover Mighty Instance Management Techniques for Developing WCF Apps» из «MSDN Magazine», июнь 2006 г.
Управление экземплярами для служб уровня вызова 145 пстороне клиента. Для поддержки этой и других локальных особенностей сто- роны службы в WCF определяется понятие аспектов поведения. Аспектом по- йдет (behavior) называется локальный атрибут службы, не влияющий на ее юммуникации. Клиент должен быть изолирован от аспектов поведения, и по- следние не должны проявляться в привязке или публикуемых метаданных службы. WCF определяет два типа аспектов стороны службы, которым соот- ветствуют два атрибута. ServiceBehaviorAttribute используется для настройки ас- пектов поведения службы, то есть аспектов поведения, распространяющихся на все конечные точки (все контракты и операции) службы. Атрибут ServiceBehavior применяется непосредственно к классу реализации службы В контексте этой главы атрибут ServiceBehavior используется для настройки режима управления экземплярами службы. Как показано в листинге 4.1, атрибут определяет свой- ство InstanceContextMode перечисляемого типа InstanceContextMode. Значение In- stanceContextMode определяет, какой режим управления экземплярами должен использоваться службой. Листинг 4.1. Атрибут ServiceBehaviorAttribute используется для настройки режима управления экземплярами public enum InstanceContextMode ( PerCall. PerSession. Single ) I [Attri buteUsage (Att r 1 buteTargets .Class)] public sealed class ServiceBehaviorAttribute : Attribute.... I public InstanceContextMode InstanceContextMode (get;set;) //... ) Атрибут OperationBehaviorAttribute используется для настройки аспектов по- ведения операций и распространяется только на реализацию конкретной опера- ции. Он может применяться только к методам, реализующим операцию кон- тракта, и никогда не применяется к определению операции в самом контракте. Применение OperationBehavior будет рассмотрено позднее в этой главе, а также в последующих главах. Управление экземплярами для служб уровня вызова Если тип службы настроен на активизацию уровня вызова, экземпляр службы (объект CLR) существует только в процессе обработки клиентского вызова. Ка- ждому клиентскому запросу (то есть вызов метода для контракта WCF) выде- ляется новый специализированный экземпляр службы. Следующая процедура
146 Глава 4. Управление экземплярами описывает механизм работы активизации уровня вызова; последовательность действий продемонстрирована на рис. 4.1. 1. Клиент обращается с вызовом к посреднику, который перенаправляет вызов службе. 2. WCF создаст экземпляр службы и вызывает метод. 3. При возврате управления методом, если объект реализует IDisposable, WCF вызывает для него IDisposable.Dispose(). 4. Клиент обращается с вызовом к посреднику, который перенаправляет вызов службе. 5. WCF создает объект и вызывает метод. Клиент Клиент Клиент Рис. 4.1. Режим управления экземплярами уровня вызова Уничтожение экземпляра службы — весьма интересный момент. Как упоми- налось ранее, если служба поддерживает интерфейс IDisposable, WCF автомати- чески вызывает метод Dispose(), предоставляя службе возможность выполнить всю необходимую зачистку. Обратите внимание: Dispose() вызывается в том же программном потоке, который перенаправил исходный вызов метода, и при этом вызов Dispose() выполняется в контексте операции (об этом чуть позже). После вызова Dispose() WCF отсоединяет экземпляр от остальной инфраструк- туры WCF, и он становится кандидатом для уборки мусора. Преимущества служб уровня вызова В классической модели программирования «клиент-сервер» с использованием таких языков, как C++ и С#, каждый клиент получает свой специализирован- ный объект сервера. Основная проблема такого подхода заключается в том, что он плохо масштабируется. Объект сервера может использовать высокозатрат- ные или дефицитные ресурсы — подключения к базе данных, коммуникацион- ные порты или файлы. Представьте себе приложение, которое должно обслу- живать многих клиентов. Как правило, такие клиенты создают необходимые объекты при запуске клиентского приложения и избавляются от них при завер- шении последнего. На масштабируемость модели «клиент-сервер» отрицатель- но влияет тот факт, что клиентское приложение может удерживать объекты в течение долгого времени, реально используя их в течение лишь малой доли этого времени. Выделение объекта для каждого клиента приведет к связыванию
Управление экземплярами для служб уровня вызова 147 критических или ограниченных ресурсов на долгое время и в конечном итоге к исчерпанию этих ресурсов. В более эффективной модели активизации объект выделяется клиенту толь- ко на время обработки вызова от клиента к службе. В этом случае количество объектов, созданных и поддерживаемых в памяти, совпадает с количеством од- новременно обрабатываемых вызовов, а не с количеством обслуживаемых кли- ентов. В типичной Enterprise-системе только один процент всех клиентов выда- ет параллельные вызовы (Enterprise-системы с высокой загрузкой имеют около трех процентов параллельных вызовов). Если ваша система может одновремен- но поддерживать только 100 высокозатратных экземпляров службы, это озна- чает, что в типичной ситуации она сможет обслуживать до 10 000 клиентов. Именно такую возможность предоставляет режим активизации экземпляров уровня вызова, поскольку между вызовами клиент получает ссылку на посред- ника, который не связан с реальным объектом на другом конце канала связи. Преимущество такой схемы очевидно: она позволяет избавиться от высокоза- тратных ресурсов, подолгу удерживаемых экземпляром службы до того момен- та, как клиент избавится от посредника. Аналогичным образом, захват ресурсов откладывается до того момента, как они фактически потребуются клиенту. Следует учесть, что многократное создание и уничтожение экземпляра службы на стороне службы без разрыва связи с клиентом (с посредником на стороне клиента) обходится гораздо дешевле, чем создание экземпляра и под- ключения. Второе преимущество заключается в том, что повторное выделение ресурсов (или подключение к ним) экземпляра службы при каждом вызове хо- рошо укладывается в модель транзакционного управления ресурсами и тран- закционного программирования (см. главу 7), поскольку оно упрощает задачу согласования с состоянием экземпляра. Третье преимущество служб уровня вызова — возможность их применения с очередями вызовов без подключения (см. главу 9), благодаря упрощенному отображению экземпляров служб на от- дельные сообщения в очереди. Настройка служб уровня вызова Чтобы пометить тип службы как службу уровня вызова, следует применить к нему атрибут ServiceBehavior со свойством InstanceContextMode, равным Instan- ceContextMode. PerCall: [ServiceContract] interface IMyContract (...) [Servi ceBehavi or(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyService : IMyContract (...) В листинге 4.2 приведена простая служба уровня вызова и ее клиент. Как видно из выходных данных программы, для каждого вызова метода клиента конструируется новый экземпляр службы.
148 Глава 4. Управление экземплярами Листинг 4.2. Служба уровня вызова и клиентский код ///////////////// Сторона службы ///////////////// [ServiceContract] interface IMyContract { [Operationcontract] void MyMethodO: } [Servi ceBehavi or(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyService : IMyContract.IDisposable { int m_Counter = 0; MyServiceO { Trace.Wr1teL1ne("MyServ1ce.MyServ i ce()"): } public void MyMethodO { m_Counter++: Trace. Wri teLineCCounter = " + m_Counter); } public void Disposed { Trace. Wri teLine( "MyService. Disposed"): } } ///////////////// Сторона клиента ///////////////// MyContractClient proxy = new MyContractClient(); proxy.MyMethod(): proxy.MyMethod(); proxy.CloseO; // Результат MyService.MyServi ce() Counter = 1 MyService. Di sposeO MyService. MyServiceO Counter = 1 MyService.DisposeO Проектирование служб уровня вызова Хотя теоретически режим активизации уровня вызова может использоваться для любого типа службы, на практике служба и ее контракты должны изначаль- но проектироваться с расчетом на поддержку этого режима. Главная проблема заключается в том, что клиент не знает, что он каждый раз имеет дело с новым экземпляром. Службы уровня вызова должны быть службами с контролем со- стояния (state-aware); другими словами, они должны производить упреждаю- щее управление своим состоянием, создавая иллюзию непрерывного сеанса. Службы с контролем состояния следует отличать от служб без состояния (stateless). Если бы службы уровня вызова были полноценными службами без
Управление экземплярами для служб уровня вызова 149 состояния, исчезла бы исходная необходимость в применении активации уров- ня вызова. Режим уровня вызова нужен именно потому, что службы обладают состоянием, и притом высокозатратным. Экземпляр службы уровня вызова создается непосредственно перед каждым вызовом метода и уничтожается сразу же после каждого вызова. Следовательно, в начале вызова объект должен ини- циализировать свое состояние по данным из некоего хранилища, а в конце вы- зова - вернуть измененное состояние в это хранилище. Как правило, в качестве такого хранилища используется база данных или файловая система, но также им могут быть временные области памяти вроде статических переменных. Впрочем, не все состояние объекта может сохраняться «как есть». Напри- мер, если состояние содержит подключение к базе данных, объект должен зано- во установить подключение при конструировании или в начале каждого вызова и уничтожить его в конце вызова или своей реализации IDisposable. Dispose(). Ре- жим уровня вызова подразумевает одно важное следствие для архитектуры операций: каждая операция должна включать параметр для идентификации эк- земпляра службы, состояние которого требуется получить. При помощи этого параметра экземпляр загружает из хранилища именно свое состояние, а не со- стояние другого экземпляра того же типа. Примеры таких параметров — номер счета для службы управления банковскими счетами, номер заказа для службы обработки заказов и т. д. В листинге 4.3 представлен шаблон для реализации службы уровня вызова. Листинг 4.3. Реализация службы уровня вызова [DataContract] class Param (...) [ServiceContract] interface IMyContract ( [Operationcontract] void MyMethodt Pa ram stateidentifier); ) [Servi ceBehav ior(InstanceContextMode = InstanceContextMode.PerCai1)] class MyPerCallService : IMyContract,IDisposable ( public void MyMethodtParam stateidentifier) GetStatetstateldentifier); DoWorkt); SaveStatetstateldentifier); ) void GetStatetParam stateidentifier) (...) void DoWorkt) (...) void SaveState(Param stateidentifier) (...) void void DisposeO
150 Глава 4. Управление экземплярами Класс реализует операцию MyMethod(), которая получает параметр типа Param (псевдотип, придуманный для этого примера), идентифицирующий эк- земпляр: public void MyMethod(Param stateidentifier): Затем экземпляр использует идентификатор для загрузки состояния и его сохранения в конце вызова метода. Фрагменты состояния, общие для всех кли- ентов, инициализируются в конструкторе и уничтожаются в Dispose(). Режим активизации уровня вызова лучше всего работает тогда, когда объем работы при каждом вызове метода является конечной величиной, и не сущест- вует дополнительных действий, завершаемых в фоновом режиме после возвра- та управления методом. По этой причине вызовы не должны порождать фоно- вые программные потоки или возвращать экземпляру асинхронные вызовы, потому что после возврата из метода объект будет уничтожен. Так как службы уровня вызова загружают свое состояние из некоего хранилища при каждом вызове, они хорошо работают в сочетании с механизмами распределения за- грузки, при условии, что хранилище состояний представляет собой глобальный ресурс, доступный для всех компьютеров. Распределитель нагрузки может пе- ренаправлять вызовы разным компьютерам по своему усмотрению, зная, что каждая служба уровня вызова сможет обработать вызов после загрузки состоя- ния. Службы уровня вызова и производительность Службы уровня вызова представляют собой явный компромисс между произ- водительностью (затраты на повторное конструирование состояния экземпляра при каждом вызове метода) и масштабируемостью (сохранение состояния и связанных с ним ресурсов). Не существует однозначных правил относительно того, когда и до какой степени можно поступиться долей производительности ради значительного выигрыша в масштабируемости. Возможно, вам придется провести профильный анализ своей системы, и в конечном счете спроектиро- вать одни службы для использования активации уровня вызова, и отказаться от нее в других службах. Операции зачистки Поддерживает ли тип службы IDisposable или нет — подробность реализации, несущественная для клиента. В конце концов, у клиента все равно нет никакой возможности вызвать метод Dispose(). При проектировании контракта для служ- бы уровня вызова старайтесь избегать определения операций, полностью по- священных зачистке состояния и ресурсов: // Не рекомендуется [ServiceContract] interface IMyContract { void DoSomethingO; void Cleanup!); } [ServiceBehaviordnstanceContextMode = InstanceContextMode.PerCai 1)] class MyPerCallService : IMyContract.IDisposable
Сеансовые службы 151 I public void DoSomething() (...) public void Cleanup() public void DisposeO I CleanupO: ) ) Неразумность такой архитектуры очевидна. Вызов метода зачистки клиен- том приведет к нежелательному эффекту: объект создается только для того, чтобы клиент мог вызвать для него Cleanup(). Но сразу же после этого WCF вы- зовет метод IDisposable.Dispose() (если он присутствует) для повторного выпол- нения той же зачистки. Выбор служб уровня вызова Хотя модель программирования служб уровня вызова выглядит несколько чу- жеродной для разработчиков «клиент-сервер», в действительности именно ак- тивизация уровня вызова является предпочтительным режимом управления экземплярами для служб WCF. Первый аргумент в пользу служб уровня вызо- ва предельно прост: они лучше масштабируются. При проектировании службы я стремлюсь заложить в нее 10-кратный допуск по масштабируемости. Иначе говоря, каждая служба проектируется так, чтобы она справилась с нагрузкой по крайней мере на порядок большей, чем указано в требованиях. Дело в том, что в любой проектировочной области инженер никогда не проектирует систему для работы с точно заданной номинальной загрузкой. Вряд ли вам захочется входить в здание, несущие конструкции которого выдерживают только задан- ную в задании нагрузку, или садиться в лифт, выдерживающий ровно шесть пассажиров, как сказано в инструкции и т. д. С программными системами дело обстоит точно так же — зачем проектировать систему для конкретной текущей нагрузки, если все остальные работники трудятся над расширением бизнеса, что неминуемо приведет к увеличению нагрузки? Программные системы долж- ны работать годами, выдерживая не только текущие, но и будущие нагрузки. В результате при использовании «золотого правила 10Х» у вас очень быстро возникнет потребность в масштабировании, обеспечиваемом службами уровня вызова. Второй аргумент в пользу служб уровня вызова — транзакции. Как бу- дет показано в главе 7, транзакции являются абсолютно необходимой частью любой системы, и службы уровня вызова очень хорошо укладываются в тран- закционную модель программирования независимо от системной нагрузки. Сеансовые службы WCF может поддерживать сеанс между клиентом и определенным экземпля- ром службы. Когда клиент создает нового посредника для службы, настроенной как сеансовая служба, клиент получает новый специализированный экземпляр
152 Глава 4. Управление экземплярами службы, не зависящий от всех остальных экземпляров той же службы. Экземп- ляр продолжает существовать до тех пор, пока не станет ненужным клиенту. Режим активизации сильно напоминает классическую модель «клиент-сервер». Иногда этот режим также обозначается термином «приватный сеанс». Каждый приватный сеанс однозначно связывает посредника и его набор каналов на сто- роне клиента и службы с определенным экземпляром службы (точнее, с его контекстом, как будет показано далее). Так как экземпляр службы остается в памяти на протяжении всего сеанса, он может использоваться для хранения состояния, а модель программирования очень близка к классической модели «клиент-сервер». Соответственно, ей при- сущи те же проблемы масштабируемости и транзакционности, что и классиче- ской модели «клиент-сервер». Службы, настроенные на использование приват- ных сеансов, обычно не могут поддерживать более нескольких десятков (воз- можно, до сотни или двух) клиентов из-за затрат ресурсов, связанных с каждым специализированным экземпляром службы. ПРИМЕЧАНИЕ------------------------------------------------------------- Клиентский сеанс определяется на уровне конкретной конечной точки службы конкретного посред- ника. Если клиент создаст другого посредника для той же или другой конечной точки, второй по- средник будет связан с новым экземпляром и сеансом. Настройка приватных сеансов Поддержка сеанса складывается из трех составляющих: поведения, привязки и контракта. Первая часть необходима для того, чтобы платформа WCF под- держивала существование экземпляра службы на протяжении сеанса и переда- вала ему клиентские сообщения. Для этого свойству InstanceContextMode атри- бута ServiceBehavior задается значение InstanceContextMode.PerSession: [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)] class MyService : IMyContract {•••} Поскольку InstanceContextMode.PerSession является значением по умолчанию для свойства InstanceContextMode, следующие определения эквивалентны: class MyService : IMyContract [ServiceBehavior] class MyService : IMyContract {...} [ServiceBehaviordnstanceContextMode = InstanceContextMode.PerSession)] class MyService : IMyContract {•••} Сеанс обычно завершается при закрытии посредника клиентом. При этом посредник оповещает службу о завершении сеанса. Если служба поддерживает IDisposable, то метод Dispose() будет вызван асинхронно с клиентом. Впрочем, №spose() вызывается в рабочем программном потоке без контекста операции.
Сеансовые службы 153 Чтобы все сообщения определенного клиента передавались конкретному эк- земпляру, WCF необходимы средства идентификации клиента. Один из спосо- бов основан на сеансах транспортного уровня, то есть непрерывного поддержа- ния связи на транспортном уровне (например, протоколов TCP и IPC). В резуль- тате при использовании привязок NetTcpBinding и NetNamedPipeBinding WCF свя- зывает это подключение с клиентом. Ситуация усложняется с протоколом HTTP, не обладающим состоянием. На концептуальном уровне каждое сообщение HTTP достигает службы по новому подключению, поэтому поддержка сеанса транс- портного уровня для BasicHttpBinding невозможна. С другой стороны, привязка WS позволяет эмулировать сеанс транспортного уровня — для этого в заголов- ки сообщений включается логический идентификатор сеанса, однозначно иден- тифицирующий клиента. Фактически WSHttpBinding эмулирует транспортный сеанс при включении безопасного или надежного обмена сообщениями. Контрактная составляющая поддержки сеанса необходима из-за того, что runtime-среда WCF на стороне клиента должна знать о необходимости исполь- зования сеанса. Атрибут ServiceContract предоставляет свойство SessionMode пе- речисляемого типа SessionMode: public enum SessionMode Allowed. Required, NotAllowed 1 [AttributeUsage(Attri buteTargets. Interface | Attri buteTargets. Cl ass, Inherited=false)] public sealed class ServiceContractAttribute : Attribute ( public SessionMode SessionMode (get:set:} //... 1 Свойство SessionMode по умолчанию равно SessionMode.Allowed. Заданное значение SessionMode включается в метаданные службы и правильно воспроиз- водится при импортировании метаданных контракта клиентом. SessionMode.Allowed Значение SessionMode.Allowed используется по умолчанию, поэтому следующие определения эквивалентны: [ServiceContract] interface IMyContract (...) [ServiceContract (SessionMode = SessionMode.Allowed)] interface IMyContract (...) Свойство SessionMode относится не к режиму создания экземпляров, а к под- держке сеансов транспортного уровня (или ее эмуляции в случае привя- зок WS). Как подсказывает имя, задание свойству SessionMode значения SessionMode.Allowed разрешает транспортные сеансы, но не делает их обязатель-
154 Глава 4. Управление экземплярами ними. Итоговое поведение определяется конфигурацией службы и используе- мой привязкой. Если служба настроена на активизацию уровня вызова, она продолжает вести себя как служба уровня вызова, как в листинге 4.2. Если служба настроена как сеансовая, она ведет себя как сеансовая служба только в том случае, если используемая привязка поддерживает сеансы транспортного уровня. Например, привязка BasicHttpBinding принципиально не поддерживает сеансы транспортного уровня, поскольку протокол HTTP по своей природе не имеет состояния. Привязка WSHttpBinding без безопасности и надежной достав- ки сообщений тоже не поддерживает сеансов транспортного уровня. В обоих указанных случаях, даже если служба настроена в режиме InstanceContextMode. PerSession, а контракт — в режиме SessionMode.Allowed, служба ведет себя как служба уровня вызова, а вызовы Dispose() осуществляются асинхронно; иначе говоря, работа клиента не блокируется после вызова, пока WCF занимается уничтожением экземпляра. Тем не менее при использовании привязки WSHttpBinding с поддержкой безо- пасности (конфигурация по умолчанию) или надежной доставки сообщений, а также привязок NetTcpBinding и NetNamedPipeBinding, служба ведет себя как се- ансовая. Например, при использовании NetTcpBinding следующая служба будет обладать сеансовой поддержкой: [ServiceContract] Interface IMyContract {...} class MyService : IMyContract (••} Обратите внимание: в приведенном фрагменте просто используются значе- ния по умолчанию для свойств SessionMode и InstanceContextMode. SessionMode.Required Значение SessionMode.Required разрешает использование сеансов транспортного уровня, но не гарантирует поддержки сеансов прикладного уровня. Контракт, в конфигурацию которого входит SessionMode.Required, не может использовать- ся с конечной точкой службы, привязка которой не поддерживает сеансов транспортного уровня; это ограничение проверяется во время загрузки службы. Впрочем, службу можно настроить как службу уровня вызова, экземпляры ко- торой создаются и уничтожаются при каждом вызове со стороны клиента. Только в том случае, если служба настроена как сеансовая, ее экземпляр будет существовать на протяжении всего клиентского сеанса: [ServiceContract(SessionMode = SessionMode.Required)] interface IMyContract (...) class MyService : IMyContract {...} ПРИМЕЧАНИЕ ---------------------------------------------------------------------- При проектировании сеансовых контрактов я рекомендую явно задавать SessionMode.Required, не полагаясь на значение по умолчанию SessionMode.Allowed. Во всех примерах книги там, где архи- тектура подразумевает сеансовое взаимодействие, активно применяется SessionMode.Required.
Сеансовые службы 155 В листинге 4.4 приведены те же служба и клиент, что и в листинге 4.2, за ис- ключением того, что контракт и служба настроены на использование приватно- го сеанса. Как видно из выходных данных, клиенту выделяется специализиро- ванный экземпляр службы. Листинг 4.4. Сеансовая служба и клиентский код ///////////////// Сторона службы ///////////////// [ServiceContract (SessionMode = SessionMode.Required)] interface IMyContract I [Operationcontract] void MyMethod(); ) class MyService : IMyContract. IDisposable I int m_Counter - 0; MyServiceO ( Trace.WriteLineC'MyService.MyService()"): I public void MyMethodO m_Counter++; Trace. Wri teLineC Counter = ” + m_Counter); I public void Dispose!) ( Trace. Wri teLi ne( "MyService .Dispose!)"): I ) Illi 1111 / / / /11 /11 Сторона клиента 11111111111111111 MyContractClient proxy = new MyContractCllento: proxy. MyMethod (): proxy. MyMethod (); proxy.Closet); // Результат MyService.MyServiceO Counter = 1 Counter = 2 MyService.DisposeO SessionMode.NotAllowed Значение SessionMode.NotAllowed запрещает использование сеансов транспорт- ного уровня, что автоматически делает невозможными сеансы прикладного уровня. Независимо от конфигурации, служба всегда ведет себя как служба уровня вызова. Если служба реализует IDisposable, то метод Dispose() вызывает- ся асинхронно по отношению к клиенту; другими словами, после вызова управ- ление возвращается клиенту, a WCF вызывает Dispose() в фоновом режиме
156 Глава 4. Управление экземплярами (в программном потоке входящего вызова). Поскольку протоколы TCP и IPC поддерживают сеансы на транспортном уровне, вам не удастся настроить ко- нечную точку службы, предоставляющую контракт с пометкой SessionMode.Not- Allowed и при этом использующую привязку NetTcpBinding или NetNamedPipe- Binding; данное ограничение проверяется во время загрузки службы. Тем не менее использование WSHttpBinding с эмуляцией транспортных сеансов разре- шено. Чтобы программный код лучше читался, я рекомендую при выборе SessionMode.NotAllowed всегда настраивать службу как службу уровня вызова: [Serv1ceContract(SessionMode = SessionMode.NotAllowed)] Interface IMyContract [ServiceBehaviordnstanceContextMode = InstanceContextMode.PerCall)] class MyService : IMyContract Привязка BasicHttpBinding не может иметь сеансов транспортного уровня, поэтому использующие ее конечные точки ведут себя так, как если бы контракт всегда настраивался со значением SessionMode.NotAllowed, а вызов Dispose() все- гда происходит асинхронно. Привязка, контракт и поведение службы В табл. 4.1 представлена сводка режимов управления экземплярами, используе- мых в зависимости от привязки, сеансового режима контракта и контекста, за- данного в поведении службы. В таблице отсутствуют недопустимые комбина- ции — например, SessionMode.Required с BasicHttpBinding. Таблица 4.1. Зависимость режима управления экземплярами от привязки, конфигурации контракта и поведения службы Привязка Сеансовый режим Контекстный режим Асинхронный вызов Dispose() Режим управления экземплярами Basic Allowed/NotAllowed PerCall/PerSession Да PerCall TCP, IPC Allowed/Required PerCall Нет PercCall TCP, IPC Allowed/Required PerSession Да PerSession WS (безопасность и надежность отсутствуют) NotAllowed/Allowed PerCall/PerSession Да PerCall WS (с безопасностью Allowed/Required или надежностью) PerSession Да PerSession WS (с безопасностью или надежностью) NotAllowed PerCall/PerSession Да PerCall Согласованные конфигурации Если один контракт, реализуемый службой, является контрактом с состоянием, все остальные контракты настоятельно рекомендуется сделать контрактами с состоянием, чтобы избегать смешения контрактов уровня вызова и уровня се- анса в одном типе службы, хотя WCF это и позволяет:
Сеансовые службы 157 [ServiceContract (SessionMode = SessionMode.Required)] interface IMyContract {...) [ServiceContract (SessionMode = SessionMode.NotAllowed)] interface IMyOtherContract (...) II He рекомендуется class MyService : IMyContract. IMyOtherContract (...) Причина очевидна: службы уровня вызова должны активно управлять сво- им состоянием, а сеансовые службы — нет. Хотя два контракта будут предо- ставляться по двум разным конечным точкам и могут независимо использовать- ся двумя разными клиентами, такой подход обернется громоздкой, усложнен- ной реализацией класса службы. Сеансы и надежность Сеанс между клиентом и экземпляром службы надежен лишь в той степени, в какой надежен нижележащий транспортный сеанс. Соответственно, у служ- бы, реализующей контракт с состоянием, все конечные точки, предоставляю- щие этот контракт, должны использовать привязки с поддержкой надежных транспортных сеансов. Проследите за тем, чтобы во всех случаях использова- лась привязка с поддержкой надежности и последняя была явно включена на стороне как клиента, так и службы — либо на программном, либо на админист- ративном уровне, как показано в листинге 4.5. Листинг 4.5. Включение надежности для сеансовых служб <!--Конфигурация хоста <system. servi ceModel > <services> <service name = "MyPerSessionService"> <endpoint address = "net.tcp://1 oca 1 host:8000/MyPerSess ionServi ce" binding = "netTcpBinding" bindingconfiguration = "TCPSession" contract = "IMyContract" /> </service> </services> <bindings> <netTcpBi ndi ng> <binding name = "TCPSession"> <reliableSession enabled = "true"/> </binding> </netTcpBinding> </bindings> </system. servi ceModel > <!-Конфигурация клиента:--> продолжение^
158 Глава 4. Управление экземплярами <system.servi ceModel> <cl1ent> <endpoint address = "net.tcp://localhost:8000/MyPerSessionService" binding = "netTcpBinding" bindingconfiguration = "TCPSession" contract = "IMyContract" /> </client> <bindings> <netTcpBinding> <binding name = "TCPSession"> <reliableSession enabled = "true"/> </binding> </netTcpBinding> </bindings> </system.servi ceModel> Единственное исключение из этого правила составляют привязки именован- ных каналов. Они не нуждаются в протоколах надежной передачи сообщений (все вызовы все равно происходят в пределах одного компьютера), поэтому та- кой транспорт считается изначально надежным. Как надежный транспортный сеанс, так и упорядоченная доставка сообщений не являются обязательными, но WCF обеспечит сеанс даже при отключении упорядоченной доставки. Оче- видно, вследствие самой природы прикладных сеансов, взаимодействующий с сеансовой службой клиент ожидает, что все сообщения будут доставляться в порядке их отправки. К счастью, упорядоченная доставка включается по умолчанию при включении надежного транспортного сеанса, так что дополни- тельная настройка не потребуется. Идентификатор сеанса Каждый сеанс обладает уникальным идентификатором, доступным как для клиента, так и для службы. Идентификатор сеанса представляет собой GUID (глобально-уникальный идентификатор) и может использоваться для ведения журналов и диагностики. Служба обращается к идентификатору сеанса через контекст вызова операции. У каждой операции службы имеется контекст вызо- ва операции — набор свойств, используемых наряду с идентификатором сеанса для обратных вызовов, управления транзакциями, безопасности, обращения к хосту и обращений к объекту, представляющему сам контекст выполнения. Класс Operationcontext предоставляет доступ ко всем этим свойствам, а служба получает ссылку на контекст операции текущего метода при помощи статиче- ского метода Current класса Operationcontext: public sealed class Operationcontext : { public static Operationcontext Current {get:set:} public string Sessionld {get:}
Сеансовые службы 159 Чтобы получить идентификатор сеанса, служба читает значение свойства Sessionld, которое возвращает GUID в форме строки: string sessionld = Operationcontext .Current .Sessionld; Trace. Wri teLi ne( sessi onld); II Трассировка: //unv.uuid :c8141f66--51a6-4c66-9e03-927d5ca97153 Если служба уровня вызова без транспортного сеанса (например, с привяз- кой BasicHttpBinding или режимом SessionMode.NotAllowed) обращается к свойст- ву Sessionld, свойство вернет null — сеанса не существует, а следовательно, нет и идентификатора. ПРИМЕЧАНИЕ---------------------------------------------------------------------------- В методе IDisposable. Dispose() служба не имеет контекста операции, а следовательно, не может об- ращаться к идентификатору сеанса. Клиент получает идентификатор сеанса через посредника. Как упоминалось в главе 1, класс ClientBase<T> является базовым классом посредников, генери- руемых Visual Studio 2005 и SvcUtil. ClientBase<T> предоставляет свойство Inner- Channel класса IClientChannel, доступное только для чтения. Тип IClientChannel наследует от интерфейса IContextChannel, предоставляющего свойство Sessionld, которое возвращает идентификатор сеанса в форме строки: public interface IContextChannel I string Sessionld (get;} //... } public interface IClientChannel : IContextChannel.... (...) public abstract class ClientBase<T> : ... ( public IClientChannel InnerChannel (get;} //... Для определений из листинга 4.4 получение идентификатора сеанса клиен- том может происходить примерно так: MyContractClient proxy = new MyContractClient(); proxy. MyMet hod (); string sessionld = proxy. InnerChannel .Sessionld; Trace.WriteLine(sessionld); // Трассировка: //urn:uuid:c8141f66-51a6-4c66-9eO3-927d5ca97153 Тем не менее, до какой степени клиентский идентификатор сеанса совпадает с идентификатором сеанса службы и вообще разрешено ли клиенту обращаться к свойству Sessionld, зависит от используемой привязки и ее конфигурации. Идентификаторы сеансов на сторонах клиента и службы связаны надежным се- ансом на транспортном уровне. Если используется привязка TCP с включен-
160 Глава 4. Управление экземплярами ным надежным сеансом (как это должно быть), клиент может получить дейст- вительный идентификатор сеанса только после вызова первого метода службы, обозначающего начало сеанса, или явного открытия посредника. Если обраще- ние производится до первого вызова, свойство Sessionld задается равным null Идентификатор сеанса, получаемый клиентом, совпадает с идентификатором службы. Если используется привязка TCP, но надежный сеанс отключен, кли- ент может обратиться к идентификатору сеанса до первого вызова, однако по- лученное значение будет отличаться от получаемого службой. Со всеми при- вязками WS и надежным обменом сообщениями, идентификатор сеанса равен null до первого вызова (или открытия посредника), но после него клиент и служба всегда обладают одинаковыми идентификаторами сеанса. Если на- дежная передача сообщений отсутствует, перед обращением к идентификатору сеанса необходимо сначала использовать посредника (хотя бы просто открыть его), иначе вы рискуете получить исключение InvalidOperationException. После открытия посредника клиент и служба обладают совпадающими идентифика- торами сеансов. При использовании привязок именованных каналов клиент может обратиться к свойству Sessionld до первого вызова, но получаемое им значение идентификатора всегда будет отличаться от значения службы. Следо- вательно, при использовании привязок именованных каналов идентификатор сеанса лучше полностью игнорировать. Завершение сеанса Как правило, сеанс завершается с закрытием посредника клиентом. Тем не ме- нее в случае некорректного завершения клиента или возникновения проблем со связью для каждого сеанса устанавливается тайм-аут, по умолчанию рав- ный 10 минутам. Сеанс автоматически завершается через 10 минут бездействия со стороны клиента, даже если клиент собирается использовать его в будущем. Если клиент попытается использовать посредника после завершения сеанса по тайм-ауту, он получит исключение CommunicationObjectFaultedException. Клиент и служба могут задать разные величины тайм-аутов. Привязки, поддерживаю- щие надежные сеансы транспортного уровня, могут предоставить свойство Reli- ableSession типа ReliableSession или OptionalReliableSession. Класс ReliableSession обладает свойством TimeSpan InactivityTimeout, которое может использоваться для задания нового тайм-аута по бездействию: public class ReliableSession { public TimeSpan InactivityTimeout {get:set;} //... } public class OptionalReliableSession : ReliableSession { public bool Enabled {get:set;} //... } public class NetTcpBinding : Binding....
public Optional ReliableSession ReliableSession {get:} II... I public abstract class WSHttpBindingBase : public OptionalReliableSession ReliableSession {get:} //... public class WSHttpBinding : WSHttpBindingBase,... () public class WSDualHttpBinding : Binding,... public ReliableSession ReliableSession (get:} II... ) Например, следующий фрагмент на программном уровне устанавливает 25-минутный тайм-аут для привязки TCP: NetTcpBinding tcpSessionBinding = new NetTcpBinding(): tcpSessionBindi ng. Rel i abl eSessi on. Enabled = true: tcpSess i onBi ndi ng. Rel i abl eSes s i on. I nact i v i tyTi meout = T i meSpan.F romMi nutes(25): А вот как выглядит эквивалентный фрагмент в конфигурационном файле: <netTcpBinding> <binding name = "TCPSession"> <reliableSession enabled = "true" inactivityTimeout = "00:25:00"/> </binding> </netTcpBinding> Если клиент и служба задают разные тайм-ауты, используется более корот- кий интервал. Синглетные службы Если служба настроена в синглетном режиме управления экземплярами, все клиенты независимо друг от друга подключаются к единственному экземпляру службы, обслуживающему все конечные точки. Срок существования синглет- ной службы не ограничен, и она уничтожается только при завершении хоста. Единственный экземпляр синглетной службы создается ровно один раз — при создании хоста. Использование синглетной службы не требует, чтобы клиент поддерживал сеансы или использовал привязку, поддерживающую сеанс транспортного уровня. Если контракт, потребляемый клиентом, обладает поддержкой сеансов, то во время вызова синглетная служба будет обладать тем же идентификатором сеанса, что и клиент (если это допускает привязка); но при закрытии клиента посредник только завершает сеанс, не уничтожая единственного экземпляра. Если синглетная служба поддерживает контракты без сеансов, эти контракты
162 Глава 4. Управление экземплярами не будут создавать экземпляры на уровне вызова; они будут связаны с тем же единственным экземпляром. По самой своей природе синглетная служба ори- ентирована на совместное использование, и каждый клиент просто создает для нее своих посредников. Чтобы настроить службу на синглетный режим активизации, задайте свой- ству InstanceContextMode значение InstanceContextMode.Single: [Serv1ceBehav1or(InstanceContextMode = InstanceContextMode.Single)] class MySingleton : {•••} В листинге 4.6 представлена синглетная служба с двумя контрактами: один требует поддержки сеанса, а другой нет. Как видно из клиентского вызова, об- ращения к двум разным конечным точкам передаются одному экземпляру, а за- крытие посредников не приводит к уничтожению этого экземпляра. Листинг 4.6. Синглетная служба с двумя контрактами ///////////////// Код службы ///////////////// [ServiceContract(SessionMode = SessionMode.Required)] Interface IMyContract { [Operationcontract] void MyMethod(); } [Serv1ceContract(Sess1onMode = SessionMode.NotAllowed)] Interface IMyOtherContract { [Operationcontract] void MyOtherMethodO; } [ServiceBehav1or(InstanceContextMode = InstanceContextMode.Single)] class MySingleton : IMyContract.IMyOtherContract.IDisposable { Int m_Counter = 0: public MySIngletonO { T race.Wri teLi ne("MvSi ngleton.MySingleton!)"): } public void MyMethodO { m_Counter++; Trace. Wri teLi neCCounter = " + m_Counter): } public void MyOtherMethodO { m_Counter++; Trace. Wri teLi neCCounter = " + m_Counter): } public void Disposed { Trace. WriteLineC "Si ngl eton. Di sposed"); } } ///////////////// Код клиента /////////////////
Синглетные службы 163 MyContractCllent proxy1 = new MyContractCl ientt); proxy 1. MyMethodt): proxy 1. Cl ose(); MyOtherContractCl 1 ent proxy2 = new MyOtherContractClientt): proxy2. MyOtherMethod (); proxy2. Closet): И Результат MySingl eton. MySingl eton () Counter = 1 Counter = 2 Инициализация синглетной службы Иногда для создания и инициализации синглетной службы оказывается недос- таточно конструктора по умолчанию. Например, для инициализации состояния могут потребоваться некие особые действия или информация, недоступная для клиентов (или не относящаяся напрямую к их работе). Для подобных ситуаций WCF позволяет напрямую создать экземпляр синглетной службы до нормаль- ного создания экземпляра CLR, инициализировать его, а затем открыть хост с этим экземпляром как синглетную службу. У класса ServiceHost имеется спе- циальный конструктор, которому при вызове передается объект: public class ServiceHost : ServiceHostBase.... public ServiceHost(object singletoninstance. params Uri[] baseAddresses): public virtual object Singletoninstance (get:} II... ) Учтите, что передаваемый объект должен быть настроен как синглетный эк- земпляр. Для примера рассмотрим код в листинге 4.7. Класс MySingleton снача- ла инициализируется, а затем передается в качестве синглетного экземпляра при создании хоста. Листинг 4.7. Инициализация и создание хоста для синглетного экземпляра II Код службы [ServiceContract] interface IMyContract I [Operationcontract] void MyMethodt): ) [ServiceBehavior (InstanceContextMode = InstanceContextMode. Single)] class MySingleton : IMyContract ( int m_Counter = 0: public int Counter I продолжение &
164 Глава 4. Управление экземплярами get { return m_Counter; } set { m_Counter = value: } } public void MyMethodO { m_Coiinter++; Trace. WrlteLlneCCounter = " + m_Counter): } } // Код хоста MySingleton singleton = new MyS1ngleton(): singleton.Counter = 42: ServiceHost host = new ServlceHost(slngleton); host.Open(); // Блокирующие вызовы host.CloseO; // Клиентский код MyContractCHent proxy = new MyContractC11ent(): proxy.MyMethod(); proxy.Close(): // Результат Counter = 43 Возможно, при таком способе инициализации и хостинга синглетного эк- земпляра вы также захотите иметь возможность обратиться к нему напрямую на стороне хоста. Для этого в WCF предусмотрено свойство Singletoninstance объекта ServiceHost. Любой участник цепочки вызовов, ведущей от вызова опе- рации к синглетному экземпляру, всегда может обратиться к хосту через свой- ство Host контекста операции (свойство доступно только для чтения): public sealed class Operationcontext : ... { public ServIceHostBase Host {get:} //... } Получив ссылку на синглетный экземпляр, можно работать с ним напря- мую: ServiceHost host = Operationcontext.Current.Host as ServiceHost; Debug.Assert(host != null); MySingleton singleton - host.Singletoninstance as MySingleton: Debug.Assert(singleton !=null): singleton.Counter = 388; Если при создании хоста синглетный экземпляр не был передан, Singletoninstance возвращает null.
Синглетные службы 165 Класс ServiceHost<T> Мы можем расширить класс ServiceHost<T> (см. главу 1) и включить в него ти- пизованную инициализацию и работу с синглетными экземплярами: public class ServiceHost<T> : ServiceHost ( public ServiceHost(T si ngl eton. pa rams Uri[] baseAddresses) : base(singleton.baseAddresses) (1 public virtual T Singleton ( get ( if(SingletonInstance == null) return default(T); } return (T)Singletonlnstance: ) //... ) Параметр типа обеспечивает типизацию объекта, используемого для конст- руирования: MySingleton singleton = new MySingleton(): singleton. Counter = 42: Servi ceHost<My Si ngl eton> host = new ServiceHost<MySingleton>(singleton): host.OpenO; а также объекта, возвращаемого свойством Singleton: Servi ceHost<My Si ngl eton> host = Operationcontext.Current.Host as ServiceHost<MySingleton>; Debug. Assert (host != null): host, si ngl eton. Counter = 388: ПРИМЕЧАНИЕ------------------------------------------------------------------------------- Шаблон InProcFactory<T>, представленный в главе 1, также расширяется аналогичным образом для инициализации синглетных экземпляров. Выбор синглетного режима Синглетная служба — заклятый враг масштабируемости. Причина кроется в синхронизации состояния синглетного экземпляра. Раз вы создаете синглет- ную службу, вероятно, она обладает каким-то ценным состоянием, которое должно совместно использоваться несколькими клиентами. Проблема заключа- ется в том, что подключения нескольких клиентов к синглетному экземпляру могут происходить одновременно, и входящие клиентские вызовы будут обра- батываться в нескольких рабочих программных потоках. Синглет должен син- хронизировать доступ к своему состоянию, чтобы избежать его порчи. Соответ- ственно это означает, что в любой момент времени с ним может работать только один клиент. Такое ограничение снижает производительность, скорость
166 Глава 4. Управление экземплярами отклика и доступность до такого уровня, что синглетные службы становятся неприемлемы для систем сколько-нибудь нормального размера. Например, если операция с синглетом занимает 1/10 секунды, клиент сможет обслуживать только 10 клиентов в секунду. При большем количестве клиентов (допустим, 20 или 100) производительность системы становится неприемлемой. В общем случае синглетный объект следует использовать в том случае, если он хорошо соответствует натуральному синглету в предметной области. Нату- ральным синглетом называется ресурс, по своей природе уникальный и непо- вторимый. Примером натурального синглета служит глобальный журнал, в ко- тором все службы должны регистрировать свои действия, единственный комму- никационный порт или одно физическое устройство. Избегайте использования синглетов там, где существует хотя бы ничтожная вероятность «размножения» службы в будущем — например, установки второго аналогичного устройства или второго коммуникационного порта. Причина очевидна: если все клиенты неявно зависят от подключения к единственному и неповторимому экземпля- ру, то при появлении другого экземпляра службы клиентам вдруг потребуется способ подключения к нужному экземпляру. Это может иметь серьезные по- следствия для программной модели приложения. Из-за этих ограничений я ре- комендую по возможности избегать синглетов и поискать средства совместно- го использования состояния синглета вместо самого синглетного экземпляра. Впрочем, даже с учетом сказанного существуют случаи, в которых применение синглетных экземпляров оправдано. Демаркационные операции Иногда контракт с состоянием подразумевает определенный порядок вызова операций. Одни операции не должны вызываться первыми, другие обязательно вызываются в последнюю очередь. Для примера возьмем следующий контракт, предназначенный для обработки клиентских заказов: [ServiceContract(SessionMode = SessionMode.Required)] interface lOrderManager { [Operationcontract] void SetCustomerId(int customerld): [OperationContract] void Addltem(int itemld): [OperationContract] decimal GetTotaK); [OperationContract] bool ProcessOrders(); } Контракт обладает следующими ограничениями: клиент должен передать свой идентификатор (SetCustomerld) в первой операции сеанса, в противном случае никакие другие операции выполняться не могут; добавление позиций
Демаркационные операции 167 (Additem) и вычисление итоговой суммы (GetTotal()) выполняются в произволь- ном порядке и так часто, как пожелает клиент; обработка заказа (ProcessOrders()) завершает сеанс и поэтому должна выполняться в последнюю очередь. WCF позволяет проектировщикам контрактов особым образом помечать операции контрактов, которые могут (или не могут) завершать сеанс. Для этого используются свойства Islnitiating и IsTerminating атрибута Operationcontract: [Attri buteUs age (Attri buteTargets. Method) ] public sealed class OperrationContractAttrlbute : Attribute ( public bool Islnitiating {get:set;} public bool IsTerminating {get:set:} //... ) Применение этих свойств как бы обозначает логические границы сеанса, по- этому я буду называть подобные операции демаркационными. Во время загруз- ки службы (или при использовании посредника на стороне клиента), если этим свойствам задаются значения, отличные от значений по умолчанию, WCF про- веряет, чтобы демаркационные операции были частью контракта с обязатель- ной сеансовой поддержкой (SessionMode задано значение SessionMode.Required); в противном случае будет выдано исключение InvalidOperationException. И сеан- совые, и синглетные службы могут реализовать контракты с демаркационными операциями для управления своими клиентскими сеансами. По умолчанию свойство Islnitiating задается равным true, а свойство IsTermi- nating - равным false. Соответственно, следующие два определения эквива- лентны: [ServiceContract(Sessi onMode = SessionMode.Required)] interface IMyContract [Operationcontract ] void MyMethod(); } [ServiceContract(SessionMode = SessionMode. Requi red)] interface IMyContract I [OperationContract(Islnitiating = true. IsTerminating = false)] void MyMethodO; Как видно из листинга, оба свойства могут быть заданы для одного метода. Кроме того, по умолчанию операции не задают границы сеансов — они могут вызываться первыми, последними или между другими операциями в сеансе. Только использование других значений позволяет указать, что метод не должен вызываться первым или что он должен вызываться последним (или и то и дру- гое одновременно): [ServiceContract (SessionMode = SessionMode. Requi red)] interface IMyContract { LOperat 1 onContract ]
void StartSess1on(); [OperatlonContractdsInltiatlng = false)] void CannotStart(); [OperatlonContractdsTermlnatlng = true)] void EndSess1on(); [OperatlonContractdsInltiatlng = false. IsTermlnatlng = true)] void CannotStartCanEndSession(); } Возвращаясь к примеру с обработкой заказов, мы можем использовать де- маркационные операции для обеспечения установленных ограничений: [Serv1ceContract(Sess1onMode = SessionMode.Required)] Interface lOrderManager { [OperationContract] void SetCustomerlddnt customerld): [OperatlonContractdsInltiatlng = false)] void Addltem(1nt Itemld); [OperatlonContractdsInltiatlng = false)] decimal GetTotalO: [OperatlonContractdsInltiatlng = false. IsTermlnatlng = true)] bool ProcessOrders(): } // Клиентский код OrderManagerCllent proxy = new OrderManagerCl1ent(): proxy.SetCustomer Id(123): proxy.AddItem(4); proxy.AddItem(5); proxy.AddItem(6): proxy.Processorders(); proxy.Close(); Если свойство Islnitiating равно true (по умолчанию), это означает, что опе- рация может начать новый сеанс, если это первый вызов метода со стороны клиента, но если она вызывается после другой операции, то становится ча- стью текущего сеанса. Если Islnitiating задано значение false, это означает, что операция никогда не может вызываться первой в новом сеансе и может лишь являться частью текущего сеанса. Если свойство IsTerminating равно false (по умолчанию), это означает, что се- анс продолжается и после возврата управления операцией. Если же IsTermi- nating задается значение true, это означает, что возврат управления из вызова метода завершает сеанс, a WCF асинхронно уничтожает экземпляр службы. Клиент не сможет обратиться к посреднику с дополнительными вызовами. Уч- тите, что клиент при этом все равно должен закрыть посредника.
Деактивизация экземпляров 169 ПРИМЕЧАНИЕ------------------------------------------------------------------- бели посредник генерируется для службы, использующей демаркационные операции, импортиро- ванное определение контракта содержит значения свойств. Кроме того, WCF обеспечивает раз- дельную демаркацию на стороне клиента и службы. Деактивизация экземпляров Методика управления экземплярами служб с поддержкой состояния, описан- ная ранее, связывает клиента (или клиентов) с экземпляром службы. Тем не менее реальная картина выглядит несколько сложнее. Вспомните, о чем говори- лось в главе 1: каждый экземпляр службы существует в определенном контек- сте (рис. 4.2). Рис. 4.2. Контексты и экземпляры В действительности сеанс ассоциирует клиентские сообщения не с экземп- ляром, а с контекстом, которому этот экземпляр принадлежит. В начале сеанса хост создает новый контекст. При завершении сеанса контекст уничтожается. По умолчанию срок жизни контекста совпадает со сроком жизни экземпляра, находящегося под его управлением. Тем не менее в целях оптимизации WCF предоставляет проектировщику службы возможность разделения двух жизнен- ных циклов и деактивизации экземпляра отдельно от его контекста. Реально WCF также позволяет создать контекст, вообще не имеющий экземпляра, как показано на рис. 4.2. Я называю этот способ управления экземплярами деакти- вацией контекста. Обычно для управления деактивизацией контекста ис- пользуется свойство ReleaselnstanceMode атрибута OperationBehavior: public enum ReleaselnstanceMode I None, BeforeCa11. AfterCall. BeforeAndAfterCa 11. ) [Attri buteUsage (At t ri buteTargets. Method) ] public sealed class OperationBehaviorAttribute : Attribute,... ( public ReleaselnstanceMode ReleaselnstanceMode (get:set;} //... )
170 Глава 4. Управление экземплярами Различные значения перечисляемого типа ReleaselnstanceMode определяют момент освобождения экземпляра по отношению к вызову метода: до, после, до и после или вообще без освобождения. При освобождении экземпляра, если служба поддерживает интерфейс IDisposable, будет вызван метод Dispose(), при- чем вызов производится в контексте операции. Как правило, деактивизация экземпляра применяется либо к отдельным (но не ко всем) методам службы, либо для разных методов применяются разные ре- жимы: [ServiceContract(SessionMode = SessionMode.Required)] interface IMyContract { [Operationcontract] void MyMethod; [Operationcontract] void MyOtherMethod: } class MyService IMyContract.IDisposable { [OperationBehavior(ReleaseInstanceMode = ReleaselnstanceMode.AfterCall)] public void MyMethod() {• • •} public void MyOtherMethod() {...} public void Disposed } Причина выборочного применения понятна: при постоянном применении мы фактически получаем службу с активизацией уровня вызова и ее проще было бы сразу настроить как таковую. Если применение деактивизации пред- полагает определенный порядок вызовов, попробуйте обеспечить его при помо- щи демаркационных операций. Режим ReleaselnstanceMode.None По умолчанию свойство ReleaselnstanceMode равно ReleaselnstanceMode.None, по- этому следующие два определения эквивалентны: [OperationBehavior(ReleaseInstanceMode = ReleaselnstanceMode.None)] public void MyMethodO {•••} Сеанс Вызовы методов —“ попе “ None ж* None —w Экземпляр Рис. 4.3. Жизненный цикл экземпляра при использовании методов в режиме ReleaselnstanceMode.None
Деактивизация экземпляров 171 public void MyMethodO I.-} Значение ReleaselnstanceMode.None означает, что жизненный цикл экземпля- ра не зависит от вызова, как показано на рис. 4.3. Режим ReleaselnstanceMode.BeforeCall Если метод настроен в режиме ReleaselnstanceMode.BeforeCall, а в сеансе уже су- ществует экземпляр, перед перенаправлением вызова WCF деактивизирует его, создает новый экземпляр и предоставляет ему возможность обслужить вызов, как показано на рис. 4.4. Сеанс Вызовы методов —► BeforeCall ► None ► None —► Экземпляр Рис. 4.4. Жизненный цикл экземпляра при использовании методов в режиме ReleaselnstanceMode. BeforeCall WCF деактивизирует экземпляр и вызывает Dispose() перед вызовом метода в программном потоке входящего вызова, с блокировкой работы клиента. Тем самым гарантируется, что деактивизация будет выполнена до вызова, а не одно- временно с ним. Режим ReleaselnstanceMode.BeforeCall проектировался для опти- мизации методов типа Ореп() — методов, которые захватывают ценные ресурсы, но при этом должны освобождать выделенные ранее ресурсы. Вместо того что- бы захватывать ресурсы в начале сеанса, вы дожидаетесь вызова метода Ореп(), азатем одновременно освобождаете ранее выделенные и выделяете новые ре- сурсы. После вызова Ореп() можно переходить к вызову других методов экземп- ляра, обычно настраиваемых в режиме ReleaselnstanceMode.None. Режим ReleaselnstanceMode.AfterCall Если метод настроен в режиме ReleaselnstanceMode.AfterCall, WCF деактивизи- рует экземпляр после вызова, как показано на рис. 4.5. Сеанс Вызовы методов Экземпляр Рис. 4.5. Жизненный цикл экземпляра при использовании методов в режиме ReleaselnstanceMode.AfterCall
172 Глава 4. Управление экземплярами Схема деактивизации предназначена для оптимизации методов типа Close() — методов, освобождающих ценные ресурсы без ожидания завершения сеанса. Обычно методы ReleaselnstanceMode.AfterCall вызываются для методов, вызываемых после методов, настроенных в режиме ReleaselnstanceMode.None. Режим ReleaselnstanceMode.BeforeAndAfterCall Как подсказывает название, метод, настроенный в режиме ReleaselnstanceMode. BeforeAndAfterCall, сочетает в себе особенности ReleaselnstanceMode.BeforeCall и ReleaselnstanceMode.AfterCall. Если контекст содержал экземпляр перед вызо- вом, то непосредственно после вызова WCF деактивизирует экземпляр, создает новый экземпляр для обслуживания вызова и деактивизирует его после вызова, как показано на рис. 4.6. Сеанс Вызовы методов “ иетогеьан “ BeroreAndAnercall Апсгъан r • । । Экземпляр I • Н FH Н— Н-► Рис. 4.6. Жизненный цикл экземпляров при использовании методов в режиме ReleaselnstanceMode.BeforeAndAfterCall На первый взгляд режим ReleaselnstanceMode.BeforeAndAfterCall может пока- заться лишним, но в действительности он дополняет другие режимы. Он пред- назначен для методов, вызываемых после методов с пометкой Releaselnstance- Mode.BeforeCall или None или перед методами с пометкой ReleaselnstanceMode. AfterCall или None. Представьте ситуацию: сеансовая служба хочет пользоваться преимуществами поведения с контролем состояния (как у служб уровня вызо- ва), при этом удерживая ресурсы только тогда, когда это необходимо для опти- мизации распределения ресурсов и в целях безопасности. Если бы режим Rele- aselnstanceMode.BeforeCall был единственным вариантом, то в течение некоторого времени после вызова ресурсы оставались бы выделенными объекту, но при этом реально не использовались. Аналогичная ситуация возникнет и в том слу- чае, если доступен только вариант ReleaselnstanceMode.AfterCall: перед вызовом возникает период времени, в течение которого ресурс удерживается без исполь- зования. Явная деактивизация Вместо принимаемых на стадии проектирований решений относительного того, какие методы должны использоваться для деактивизации экземпляров, возмо- жен другой вариант: принятие решения деактивизации экземпляра во время выполнения, после возврата из метода. Задача решается вызовом метода ReleaseServiceInstance() для контекста экземпляра. Контекст экземпляра берется из свойства InstanceContext контекста операции:
Деактивизация экземпляров 173 public sealed class Instancecontext : Communicationobject.... I public void ReleaseServiceInstance(); II... ) public sealed class Operationcontext : ... I public Instancecontext Instancecontext (get;) //... ) Пример использования явной деактивизации приведен в листинге 4.8. Лиспжг 4.8. Использование метода ReleaseServiceInstance() [ServiceContract (SessionMode = SessionMode. Requi red)] interface IMyContract I [Operationcontract] void MyMethod(): ) class MyService : IMyContract.IDisposable ( public void MyMethodO ( 11 Выполнение полезной работы, а затем Operat i onContext.Current.Instancecontext.ReleaseServi celnstance(): ) public void Disposed Эффект от вызова ReleaseServiceInstance() сходен с использованием Release- InstanceMode.AfterCall. При использовании в методах с пометкой Releaselnstance- Mode. BeforeCall эффект получается таким же, как в режиме ReleaselnstanceMode. BeforeAndAfterCall. ПРИМЕЧАНИЕ------------------------------------------------------------------------------------- Деактивизация экземпляра действует и на синглетные службы, хотя такое сочетание и не имеет особого смысла — синглет по определению разрешено (и более того, желательно) никогда не деак- тивизировать. Использование деактивизации экземпляров Деактивизация экземпляров относится к средствам оптимизации; ее, как и большинство таких средств, не стоит применять в общем случае. Решение о применении деактивизации принимается только в том случае, если вам не удается добиться заданных целей по быстродействию и масштабируемости, а тщательный анализ и профилирование убедительно доказали, что деактивиза- ция экземпляров способна улучшить ситуацию. Если масштабируемость и про- изводительность являются первоочередными целями, используйте простую схему создания экземпляров на уровне вызовов и избегайте деактивизации экземпляров.
174 Глава 4. Управление экземплярами Регулирование нагрузки Хотя регулирование нагрузки и не относится напрямую к управлению экземп- лярами, она позволяет ограничивать клиентские подключения и ту нагрузку, которую они создают для вашей службы. Тем самым предотвращаются чрез- мерные нагрузки службы, выделяемых и используемых ей ресурсов. Если при включенном регулировании нагрузки превышаются пороговые значения, задан- ные вами, WCF автоматически помещает необработанные вызовы в очередь и последовательно обслуживает их по мере возможности. Если во время ожида- ния на стороне клиента происходит тайм-аут, клиент получает исключение TimeoutException. Регулирование осуществляется на уровне типа службы; други- ми словами, оно распространяется на все экземпляры службы со всеми конеч- ными точками. Для этого оно связывается с каждым диспетчером канала, ис- пользуемым службой. WCF позволяет управлять следующими параметрами использования служ- бы (некоторыми или всеми сразу): О максимальное количество параллельных сеансов — общее количество ожи- дающих обработки клиентов, имеющих сеанс транспортного уровня со служ- бой. Проще говоря, параметр определяет максимальное число ожидающих клиентов, использующих привязку TCP, IPC или любую из привязок WSc сеансами. При использовании базовой привязки или любых привязок WS без транспортного сеанса это число ни на что не влияет из-за самой природы базового соединения HTTP, не обладающего состоянием. Значение по умол- чанию равно 10; О максимальное количество параллельных вызовов — общее число вызовов, обрабатываемых в данный момент всеми экземплярами службы. Обычно чис- ло должно составлять от 1 до 3 процентов максимального количества парал- лельных сеансов. Значение по умолчанию равно 16; О максимальное количество параллельных экземпляров — фактически обозна- чает общее количество одновременно существующих контекстов. Значение по умолчанию не ограничено. Схема отображения экземпляров на контек- сты определяется режимом управления контекстами, а также правилами де- активизации контекстов и экземпляров. Для служб уровня сеанса макси- мальное количество экземпляров совпадает с общим числом параллельных активных экземпляров и общим числом параллельных сеансов. В случае применения деактивизации экземпляров количество последних может быть значительно ниже числа контекстов, но при этом клиенты все равно будут блокироваться, если количество контекстов достигнет максимального числа параллельных экземпляров. Для служб уровня вызова количество экземпля- ров фактически совпадает с количеством параллельных вызовов. Соответст- венно, максимальное количество экземпляров для службы уровня вызова является меньшим из двух чисел: максимального количества параллельных экземпляров и максимального количества параллельных вызовов. Для синг-
Регулирование нагрузки 175 летных служб максимальное количество параллельных экземпляров игнори- руется, поскольку экземпляр все равно может быть только один. ВНИМАНИЕ-------------------------------------------------------------------- Регулирование нагрузки относится к аспектам хостинга и развертывания. В процессе проектирования службы не следует делать никаких допущений относительно конфигурации ее регулирования нагруз- ки-всегда считайте, что служба берет на себя всю тяжесть клиентской нагрузки. Это объясняет, по- чему в WCF нет атрибута для такого аспекта поведения, хотя реализовать его было бы несложно. Настройка регулирования нагрузки Регулирование нагрузки обычно настраивается администратором в конфигура- ционном файле. Это позволяет по-разному регулировать нагрузку для одного кода службы в разные моменты времени или в разных местах развертывания. Хост также может настраивать регулирование нагрузки на программном уров- не, в зависимости от критериев, проверяемых на стадии выполнения. Административное регулирование нагрузки В листинге 4.9 показано, как регулирование нагрузки настраивается в конфигу- рационном файле хоста. Тег behaviorConfiguration наделяет службу пользова- тельским аспектом поведения, в котором задаются пороговые значения. Листинг 4.9. Административное регулирование нагрузки <system. servi ceModel > <services> <service name = "MyService" behaviorConfiguration = "ThrottledBehavior"> </service> </services> <behaviors> <serviceBehaviors> <behavior name = "ThrottledBehavior"> <serviceThrottl ing maxConcurrentCalIs = "12" maxConcurrentSessions = "34" maxConcurrentlnstances = "56" /> </behavior> </serviceBehaviors> </behaviors> </system. servi ceModel > Программное регулирование нагрузки Хостовой процесс может регулировать нагрузку службы на программном уров- не, в зависимости от критериев времени выполнения. Сделать это можно только до открытия хоста. Хотя хост может переопределить параметры регулирования нагрузки, заданные в конфигурационном файле, обычно программное регули- рование выполняется только при отсутствии определений в конфигурационном файле.
176 Глава 4. Управление экземплярами Класс ServiceHostBase предоставляет свойство Description типа ServiceDescrip- tion: public abstract class ServiceHostBase: ... { public ServiceDescription Description {get:} //... } Свойство, как следует из его названия, содержит описание службы со всеми ее аспектами поведения. Тип ServiceDescription содержит свойство Behaviors типа KeyedByTypeCollection<I> с использованием IServiceBehavior в качестве обобщен- ного параметра. В листинге 4.10 показано, как выполняется программная настройка регули- рования нагрузки. Листинг 4.10. Программное регулирование нагрузки ServiceHost host = new ServiceHost(typeof(MyServiсе)); ServiceThrottlingBehavior throttle: throttle - host.Description.Behaviors.Find<ServiceThrottlingBehavior>(): if(throttle == null) { throttle = new ServiceThrottlingBehavior(): throttle.MaxConcurrentCalIs = 12: throttle.maxConcurrentSessions = 34; throttle.MaxConcurrentlnstances = 56; host.Descri pti on.Behaviors.Add(throttle): } host.Open(); Сначала код хостинга убеждается в том, что аспект регулировки нагрузки не был задан в конфигурационном файле. Задача решается вызовом метода Find<T>() типа KeyedByTypeCollection<I>, с передачей ServiceThrottlingBehavior в качестве па- раметра типа. Класс ServiceThrottlingBehavior определяется в пространстве имен System.Ser- viceModel.Design: public class ServiceThrottlingBehavior : IServiceBehavior { public int MaxConcurrentCalls {get;set:} public int MaxConcurrentSessions {get;set:} public int MaxConcurrentlnstances //... } Если возвращаемое значение throttle равно null, код хостинга создает новый экземпляр ServiceThrottlingBehavior, задает его параметры и включает в коллек- цию аспектов поведения в описание службы.
Регулирование нагрузки 177 Класс ServiceHost<T> Очередное усовершенствование класса ServiceHost<T> автоматизирует выполне- ние кода, приведенного в листинге 4.10. Новая версия представлена в листин- ге4.11. Листинг 4.11. Расширение ServiceHost<T> для регулирования нагрузки public class ServiceHost<T> : ServiceHost I public void SetThrottle(int maxCalls.int maxSessions,int maxinstances) ( ServiceThrottlingBehavior throttle = new ServiceThrottlingBehaviorO: throttle.MaxConcurrentCal Is = maxCalls: throttle.MaxConcurrentSessions = maxSessions; throttle. MaxConcurrent Instances = maxinstances; SetThrottle( throttle); ) public void SetThrottle(ServiceThrottlingBehavior serviceThrottle) SetThrott 1 e (serv i ceTh rott 1 e. f a 1 se); I public void SetThrottle(ServiceThrottlingBehavior serviceThrottle. bool overrideConfig) ( iftState =- CommunicationState.Opened) ( throw new InvalidOperationExceptionCHost Is already opened"): } ServiceThrottlingBehavior throttle = Descri ption.Behaviors.Find<Servi ceThrottli ngBehavi or>(); if(throttle != null && overrideConfig == false) { return; } if(throttle != null) //overrideConfig == true, удалить конфигурацию { Description.Behaviors.Remove!throttle): } iffthrottle == null) { Descri pti on.Behaviors.Add(servi ceThrottle): } ) public ServiceThrottlingBehavior ThrottleBehavior ( get { return Description.Behaviors.Find<ServiceThrottlingBehavior>(); } } //... } Класс ServiceHost<T> предоставляет метод SetThrottle, которому передается объект регулирования нагрузки, а также логический флаг, который указывает,
178 Глава 4. Управление экземплярами нужно ли заменять настроенные значения (если они имеются). По умолчанию (в перегруженной версии SetThrottle()) используется значение false. Метод SetThrottle() проверяет, что хост еще не был открыт; для этого используется свойство State базового класса Communicationobject. Если настроенный объект конфигурации должен быть заменен, SetThrottle() удаляет его из описания. Ос- тальной код листинга 4.11 похож на листинг 4.10. Пример использования Ser- viceHost<T> для программного задания регулирования нагрузки: ServiceHost<MyService> host = new ServiceHost<MyService>(); host SetThrottle(12.32.56); host.Open(): ПРИМЕЧАНИЕ------------------------------------------------------------------------- Шаблон InProcFactory<T> (см. главу 1) аналогичным образом расширяется для поддержки регули- рования настройки. Чтение параметров регулирования нагрузки Разработчик службы может прочитать пороговые значения на стадии выполне- ния; обычно это делается для диагностических и аналитических целей. Во вре- мя выполнения экземпляр службы обращается к пороговым значениям регули- рования нагрузки из диспетчера. Прежде всего получите ссылку на хост от контекста операции. Базовый класс хоста ServiceHostBase содержит свойство ChannelDispatchers, доступное только для чтения: public abstract class ServiceHostBase : CommunicationObject.... { public Channel Dispatchercollection ChannelDispatchers {get:} //... } Тип ChannelDispatchers представляет собой типизованнную коллекцию объ- ектов ChannelDispatcherBase: public class Channel Dispatchercollection : Synchroni zedCol1ecti on<ChannelDi spatcherBase> {•••} Элементы коллекции относятся к типу ChannelDispatcher. Класс ChannelDis- patcher обладает свойством ServiceThrottle: public class ChannelDispatcher : ChannelDispatcherBase { public ServiceThrottle ServiceThrottle {get:set:} //... } public sealed class ServiceThrottle { public int MaxConcurrentCalls {get:set;} public int MaxConcurrentSessions {get:set:} public int MaxConcurrentlnstances
Регулирование нагрузки 179 (get:set:} ) Настроенные значения параметров регулирования хранятся в объекте Ser- viceThrottle: class MyService : ( public void MyMethodO // Контрактная операция ( Channel Dispatcher dispatcher - Operationcontext.Current. Host.ChannelDispatchers[O] as ChanneDispatcher: ServiceThrottle serviceThrottle = dispatcher.ServiceThrottle: Trace. WriteLine( "Max Calls = " + serviceThrottle.MaxConcurrentCalIs): Trace.WnteLineCMax Sessions = " + servi ceThrottle.Ma xConcurrentSessions): Trace.WriteLine("Max Instances = " + serviceThrottle.MaxConcurrentInstances): ) Обратите внимание: служба может только читать параметры, но не может их изменить. При попытке изменения параметров регулирования происходит ис- ключение InvalidOperation Exception. И снова класс ServiceHost<T> сделает работу с регулированием нагрузки бо- лее удобной. Сначала в класс добавляется свойство ServiceThrottle: public class ServiceHost<T> • ServiceHost ( public ServiceThrottle Throttle get ( 1f(State != CommunicatlonState.Opened) throw new InvalidOperationExceptionC'Host is not opened"): Channel Dispatcher dispatcher = Operationcontext.Current.Host.Channel Di spatchers[O] as ChannelDispatcher: return dispatcher.ServiceThrottle; ) //... Затем свойство ServiceThrottle используется для обращения к заданным пара- метрам регулирования: // Код хостинга ServiceHost<MyService> host = new ServiceHost<MyService>(): host.OpenO: class MyService : ...
180 Глава 4. Управление экземплярами public void MyMethodO // Контрактная операция ( ServiceHost<MyService> host = Operationcontext.Current. Host as ServiceHost<MyService>: ServiceThrottle serviceThrottle = host.Throttle; } } ПРИМЕЧАНИЕ ----------------------------------------------------------------------------- Обращение к свойству Throttle класса ServiceHost<T> возможно только после открытия хоста. Эго объясняется тем, что диспетчерская коллекция инициализируется только после открытия хоста. Регулируемые подключения в привязке При использовании привязок TCP и именованных каналов также можно задать максимальное количество подключений для определенной конечной точки в самой привязке. Оба класса, NetTcpBinding и NetNamedPipeBinding, содержат свойство MaxConnections: public class NetTcpBinding : Binding.... public int MaxConnections {get;set;} } public class NetNamedPipeBinding : Binding.... { public int MaxConnections {get;set;} } На стороне хоста свойство задается либо на программном уровне, либо в конфигурационном файле: <bindings> <netTcpBinding> <binding name = "TCPThrottle" maxConnections » ,,25"/> </netTcpBinding> </bindings> Значение MaxConnections по умолчанию равно 10. Если максимальное коли- чество подключений задано и на уровне привязки и в аспекте поведения служ- бы, WCF выбирает меньшее из двух значений.
Операции I В классических моделях объектно- или компонентно-ориентированного про- граммирования предусмотрен только один способ вызова метода клиентом: клиент выдает вызов, блокируется на время вызова и продолжает выполнение после возврата управления методом. Все остальные модели вызова приходится реализовывать вручную, часто с потерями для производительности и качества. Хотя WCF поддерживает классическую модель вызова, наряду с ней имеется встроенная поддержка дополнительных типов операций: односторонние вызо- вы для операций, выполняемых по принципу «вызвал и забыл», дуплексный обратный вызов для обратной передачи управления службой клиенту, а также потоковые вызовы, позволяющие клиенту и службе справляться с высокими нагрузками. В общем случае тип используемой операции входит в контракт службы и является неотъемлемой частью ее архитектуры. Тип операции даже накладывает некоторые ограничения на допустимые привязки. Соответственно, клиенты и службы должны изначально проектироваться с расчетом на опреде- ленный тип операции, и вам не удастся легко переключиться на другой тип опе- раций. Эта глава посвящена различным способам вызова операций WCF и ре- комендациям по их проектированию. Два других режима вызова операций — асинхронный вызов и очереди — рассматриваются в следующих главах1. Операции «запрос-ответ» Операции всех контрактов, приводившихся в примерах предыдущих глав, отно- сились к типу «запрос-ответ». Как подсказывает название, клиент выдает за- прос в форме сообщения и блокируется вплоть до получения ответного сообще- ния. Если служба не отвечает в течение интервала тайм-аута (по умолчанию — одна минута), клиент получает исключение TimeoutException. Режим «запрос- ответ» используется по умолчанию. Программирование операций в этом режи- ме получается достаточно простым и напоминает классическую модель «кли- ент-сервер». Возвращаемое сообщение с результатами преобразуется в обыч- 1 Глава содержит выдержки из моей статьи «WCF Essentials: What You Need to Know About One-Way Calls, Callbacks, and Events» из «MSDN Magazine», октябрь 2006 г.
182 Глава 5. Операции ные возвращаемые значения метода. Если в коммуникациях или на стороне службы возникнут какие-либо исключения, посредник выдает исключение на стороне клиента. Операции «запрос-ответ» поддерживаются всеми привязками, кроме NetPeerTcpBinding и NetMsmqBinding. Односторонние операции В некоторых случаях операция не имеет возвращаемого значения, а клиента не интересует, как завершился вызов — успехом или неудачей. Для поддержки по- добных вызовов по принципу «вызвал и забыл» в WCF предусмотрены одно- сторонние (one-way) операции. Когда клиент выдает вызов, WCF генерирует сообщение с запросом, но ответное сообщение клиенту не возвращается. Как следствие, односторонние операции не могут возвращать данные, а любые ис- ключения, инициированные на стороне сервера, не попадают на сторону клиен- та. В идеальном случае при вызове одностороннего метода клиент блокируется на минимальное время, необходимое для передачи вызова. Тем не менее на практике односторонние вызовы не тождественны асинхронным. Когда одно- сторонний вызов достигает службы, он может не обрабатываться сразу, а по- пасть в очередь на стороне службы — все зависит от настроенного режима управления параллельной обработкой (этот режим и односторонние вызовы подробно рассматриваются в главе 8). Количество сообщений (как односторон- них операций, так и операций «запрос-ответ»), помещаемых службой в очередь, определяется настройкой канала и режимом надежности. Если количество со- общений в очереди превысит ее емкость, клиент блокируется даже в случае од- ностороннего вызова. Впрочем, после постановки вызова в очередь (как это обычно происходит) блокировка снимается, и клиент продолжает выполнение, пока служба занимается обработкой операции в фоновом режиме. Односторон- ние операции поддерживаются всеми привязками WCF. Настройка односторонних операций Атрибут Operationcontract содержит логическое свойство IsOneWay: [AttributeUsage(AttributeTargers.Method)] public sealed class OperatlonContractAAttrlbute : Attribute { public bool IsOneWay {get;set;} //... } По умолчанию свойство IsOneWay равно false, что означает операцию «за- прос-ответ» (стандартный для WCF режим). Если задать IsOneWay истинное значение, метод становится односторонней операцией: [ServiceContract] Interface IMyContract { [Operationcontract(IsOneWay » true)] void MyMethodO;
Односторонние операции 183 При вызове односторонней операции от клиента не требуется никаких осо- бых действий. Значение свойства IsOneWay отражено в метаданных службы. Уч- тите, что определение контакта службы и определение, импортированное кли- ентом, должны обладать одинаковыми значениями IsOneWay. Поскольку односторонние операции не требуют ответа, возвращать из них какие-либо значения или результаты бессмысленно. Например, следующее оп- ределение односторонней операции, возвращающей значение, недействительно: ' //Недействительный контракт [SeMceContf act] interface IMyContract. I • [OperationContract(IsOneWay -- true)] int MyMethodt): ) WCF обеспечивает принудительное выполнение этого требования, проверяя сигнатуру метода при загрузке хоста. В случае несоответствия инициируется исключение InvalidOperationException. Односторонние операции и надежность Из того факта, что клиента не интересует результат вызова, вовсе не следует, что клиента не интересует, произошел вызов пли нет. В общем случае надеж- ность для служб должна быть включена, даже для односторонних вызовов — это гарантирует доставку запроса службе. Однако для односторонних вызовов клиенту может быть (а может и не быть) безразличен порядок вызова односто- ронних операций. Это одна из основных причин, по которым WCF позволяет отделить включение падежной доставки от включения упорядоченной доставки и выполнения. Разумеется, все подробности должны быть заранее согласованы между клиентом и службой; в противном случае конфигурации привязки не совпадут. Односторонние операции и сеансовые службы WCF позволяет создать сеансовый контракт с односторонними операциями: [ServiceContract (SessionMode = SessionMode.Required) J interface IMvContract ( [OperationContract (IsOneWay = true)] int MyMethodt); Если клиент выдает односторонний вызов, а затем закрывает посредника во время выполнения метода, то клиент блокируется до завершения операции. Тем не менее я считаю, что в общем случае односторонние операции в сеан- совых контрактах свидетельствуют о неграмотном проектировании. Дело в том, что сеансовая поддержка обычно подразумевает, что служба поддерживает со- стояние по поручению клиента. Любые исключения, происходящие при одно- стороннем вызове, могут нарушить состояние, но клиент об этом не узнает. Кроме того, клиент (или служба) обычно выбирает сеансовое взаимодействие
184 Глава 5. Операции из-за того, что контракт подразумевает жесткую последовательность переходов по конечному автомату состояний. Односторонние вызовы плохо укладывают- ся в такую модель. Соответственно, я рекомендую применять односторонние операции только в службах уровня вызова и синглетных службах. Если односторонние операции все же применяются в сеансовых контрактах, постарайтесь сделать так, чтобы односторонней была только последняя опера- ция, завершающая сеанс (проследите за соблюдением правил одностороннего вызова — в частности, возврата void). Чтобы обеспечить соблюдение требова- ний, воспользуйтесь демаркационными операциями: [ServiceContract(SessionMode = SessionMode.Required)] interface lOrderManager 4 { [Operationcontract] void SetCustomerId(int customerld); [OperationContractdsInitiating = false)] void Addltem(int itemld): [OperationContractdsInitiating = false)] decimal GetTotalO: [OperationContractdsOneWay - true.Islnitiating = false. IsTerminating = true)] void ProcessOrders(); } Односторонние операции и исключения Было бы неверно воспринимать односторонние операции как улицу с односто- ронним движением или «черную дыру», в которой все бесследно исчезает. Во-первых, если при вызове односторонней операции произойдет ошибка, свя- занная с коммуникационными проблемами (неверный адрес, недоступность хоста), то на стороне клиента, пытающегося вызвать операцию, будет выдано исключение. Во-вторых, в зависимости от режима управления экземплярами службы и привязки исключения на стороне службы могут распространяться на клиента. В следующем описании предполагается, что служба не инициирует FaultException или производных исключений (см. главу 6). Службы уровня вызова В случае служб уровня вызова без поддержки транспортного сеанса (например, при использовании привязок BasicHttpBinding или WSHttpBinding без надежной доставки сообщений и безопасности), если при вызове односторонней опера- ции происходит исключение, оно не отражается на клиенте, и клиент может продолжить обращаться с вызовами к тому же экземпляру посредника: [ServiceContract] interface IMyContract { [OperationContractdsOneWay = true)] int MyMethodWithError(): [Operationcontract]
Односторонние операции 185 int MyMethodWi thoutError(): ) class MyService : IMyContract I public void MethodWithErrorO ( throw new ExceptlonO; ) public void MethodW1thoutError() () ) //На стороне клиента при использовании базовой привязки: MyContractClient proxy = new MyContractClient(): proxy.MethodWI thError (): proxy.MethodWithoutError(); proxy.Close(): Тем не менее при использовании привязки WSHttpBinding с безопасностью, или NetTcpBinding без надежной доставки сообщений, или NetNamedPipeBinding, исключения на стороне службы (в том числе инициированные в результате од- носторонних операций) приводят к отказу канала, и клиент не сможет обра- титься с новыми вызовами к тому же экземпляру посредника: [ServiceContract] interface IMyContract I [Operationcontract (IsOneWay = true)] int MyMethodWi thError(): [Operationcontract] int MyMethodWi thoutError(): I class MyService : IMyContract I public void MethodWithErrorO I throw new ExceptlonO; I public void MethodWithoutErrorO I) I // На стороне клиента при использовании привязки TCP или IPC: MyContractClient proxy = new MyContractClient(): proxy. MethodWi thError (); try ( proxy.MethodWIthoutError(): // Произойдет исключение из-за отказа канала proxy.CloseO: } catch () Клиенту даже не удастся корректно закрыть посредника. При использовании WSHttpBinding или NetTcpBinding с надежной доставкой исключение не приведет к отказу канала, и клиент сможет продолжить обра- щаться с вызовами.
186 Глава 5. Операции На мой взгляд, подобные расхождения выглядят по меньшей мере стран- но — и не только потому, что выбор привязки не должен влиять на клиентский код, но и потому, что они нарушают семантику односторонних операций - вы- зывающая сторона узнает о проблемах службы при одностороннем вызове. ПРИМЕЧАНИЕ --------------------------------------------------------- Синглетная служба без поддержки сеанса в этом отношении ведет себя аналогично службе уровня вызова. Сеансовые службы и односторонние исключения Когда доходит до инициирования исключений сеансовыми службами в одно- сторонних методах, ситуация становится еще сложнее. С NetTcpBinding и Net- NamedPipeBinding исключение завершает сеанс; WCF уничтожает экземпляр службы и инициирует отказ канала. Последующие вызовы операции с исполь- зованием того же посредника приводят к исключению CommunicationException (или CommunicationObjectFaultedException, при включении надежной доставки для привязок TCP и WS), потому что ни сеанса, ни экземпляра службы более не существует. Если Close() — единственный метод, вызываемый для посредни- ка после исключения, вызов Close() инициирует CommunicationException (или CommunicationObjectFaultedException). Если клиент закрывает посредника перед возникновением ошибки, вызов Close() блокируется вплоть до ошибки, после чего Close() инициирует исключение. Это малопонятное поведение — еще одна причина избегать односторонних вызовов для сеансовых служб В случае привязок WS с транспортным сеансом исключение приводит кот- казу канала, после чего клиент не может обращаться к посреднику с новыми вызовами. Немедленное закрытие посредника после вызова, инициировавшего исключение, приводит к тем же последствиям, что и с другими привязками. ПРИМЕЧАНИЕ ---------------------------------------------------------- Синглетная служба с поддержкой сеанса в этом отношении ведет себя аналогично службе уровня вызова. Операции обратного вызова WCF дает службе возможность обращаться к клиентам с обратными вызовами. Во время обратного вызова мы имеем дело с зеркальной ситуацией: служба иг- рает роль клиента, а клиент — службы (рис. 5.1). Рис. 5.1. Механизм обратного вызова позволяет службе обращаться с вызовом к клиенту Операции обратного вызова могут использоваться в разных сценариях и приложениях, но они особенно удобны для оповещения клиентов о некото- рых событиях, произошедших на стороне сервера. Не все привязки поддержи-
Операции обратного вызова 187 вают операции обратного вызова — для этой цели могут использоваться при- | вязки с возможностью двустороннего обмена. Например, протокол HTTP не ориентирован на соединение (connectionless), поэтому по своей природе он не может использоваться для обратных вызовов; следовательно, привязки Basic- HttpBinding и WSHttpBinding делают невозможным обратный вызов. WCF обес- печивает поддержку обратного вызова для NetTcpBinding и NetNamedPipeBinding, потому что протоколы TCP и IPC по своей природе поддерживают двусторон- ние коммуникации. Для поддержки обратного вызова через HTTP в WCF име- ется привязка WSDualHttpBinding, которая в действительности создает два кана- ла HTTP: по одному передаются вызовы от клиента к службе, а по другому передаются вызовы от службы к клиенту. Контракт обратного вызова Операции обратного вызова являются частью контракта службы, причем по- следний должен определить свой контракт обратного вызова. Контракт службы может иметь не более одного контракта обратного вызова. После его определе- ния клиенты обязаны поддержать обратный вызов и предоставить конечную точку для каждого обратного вызова. Для определения контракта обратного вызова атрибут ServiceContract предоставляет свойство Callbackcontract типа Туре: [Attributeusage(Attrl buteTargers. Interface|Attrl buteTargers. Cl ass) ] public sealed class OperationContractAAttribute : Attribute I public bool Callbackcontract {get: set;} //... ) При определении контракта службы с контрактом обратного вызова необхо- димо указать атрибуту ServiceContract тип контракта обратного вызова и его оп- ределение, как показано в листинге 5.1. Листинг 5.1. Определение и настройка контракта обратного вызова interface ISomeCa 11 backContract ( [OperationContract] void OnCal 1 back (); ) [ServiceiMract (Callbackcontract = typeofC ISomeCall backContract))] interface IMyContract [OperationContract] void DoSomething(); Обратите внимание: контракт обратного вызова не нужно помечать атрибу- том ServiceContract — присутствие последнего подразумевается при определении контракта обратного вызова, и он будет включен в метаданные службы. Конеч- но, все методы интерфейса обратного вызова могут быть помечены атрибутом OperationContract.
188 Глава 5. Операции После импортирования метаданных контракта обратного вызова клиентом имя импортированного интерфейса будет отличаться от имени исходного опре- деления на стороне службы. Оно образуется из имени интерфейса контракта службы и суффикса Callback. Например, в результате импортирования опреде- лений из листинга 5.1 клиент получит следующие определения: interface IMyContractCallback { [Operationcontract] void OnCal1 back(); } [ServiceContract(CallbackContract - typeof(IMyContractCallback))] interface IMyContract { [Operationcontract] void DoSomething(); } ПРИМЕЧАНИЕ ----------------------------------------------------------------------------- Ради простоты я рекомендую даже на стороне службы присваивать контракту обратного вызова имя, построенное по той же схеме (имя интерфейса контракта службы и суффикс Callback). Настройка обратного вызова на стороне клиента В обязанности клиента входит создание объекта обратного вызова и предостав- ление конечной точки обратного вызова. Вспомните, о чем говорилось в главе 1: из всех логических уровней выполнения к экземпляру службы ближе всего нахо- дится контекст экземпляра. У класса InstanceContext имеется конструктор, кото- рый передает хосту экземпляр службы: public sealed class InstanceContext : CommunicationObject.... { public InstanceContext(object implementation); public object GetServiceInstance(); //... } Все, что требуется от клиента, — создать экземпляр объекта обратного вызо- ва и построить на его основе контекст: class MyCalIback : IMyContractCallback { public void OnCallbackO {•••} } IMyContractCallback callback = new MyCallback(); InstanceContext context = new InstanceContext(callback): Также стоит отметить, что хотя методы обратного вызова находятся на сто- роне клиента, они во всех отношениях являются операциями WCF, а следова- тельно, обладают контекстом вызова, доступным через свойство OperationCon- text.Current.
Операции обратного вызова 189 Дуплексный посредник При любых взаимодействиях с конечной точкой службы, контракт которой оп- ределяет контракт обратного вызова, клиент должен использовать посредника, поддерживающего двустороннюю передачу данных, и передать службе ссылку наконечную точку обратного вызова. Для этого посредник объявляется произ- водным от специализированного класса посредника DupLexCLientBase<T>, пред- ставленного в листинге 5.2. Листинг 5.2. Класс DuplexClientBase<T> public interface IDuplexContextChannel : IContextchannel Instancecontext Call back Instance (get:set:} //... ) public abstract class DuplexCl 1entBase<T> : Cl 1entBase<T> where T : class I protected DuplexCl 1entBase( Instancecontext callbackcontext): protected DuplexCl1entBase(Instancecontext callbackcontext, string endpointName); protected DuplexCl 1entBase(Instancecontext callbackcontext. Binding binding, EndpointAddress remoteAddress); protected DuplexCl 1entBase(object callbackinstance): protected DuplexCl 1entBase(object callbackinstance, string endpointConfigurationName): protected DuplexCl1entBase(object callbackinstance. Binding binding, EndpointAddress remoteAddress): public IDuplexContextChannel InnerDuplexChannel (get:} //... ) Клиент должен передать конструктору DupLexCLientBase<T> контекст элемен- та, под управлением которого находится объект обратного вызова (по аналогии с информацией о конечной точке службы для обычного посредника). Посред- ник конструирует конечную точку на основе контекста обратного вызова, полу- чая информацию о конечной точке обратного вызова из конфигурации конеч- ной точки службы. Конечная точка обратного вызова использует тот же тип привязки (и транспорта), что и исходящий вызов. Что касается адреса, WCF использует имя компьютера клиента и даже выбирает порт при использовании НТРР. Простая передача контекста экземпляра дуплексному посреднику и ис- пользование посредника для обращения к службе открывает доступ к конечной точке обратного вызова на стороне клиента. Для простоты DupLexCLientBase<T> также предоставляет конструкторы, которые получают объект обратного вызо- ва и «заворачивают» его в контекст во внутренней реализации. Если клиенту по какой-либо причине понадобится обратиться к контексту, DupLexCLientBase<T> также предоставляет свойство InnerDuplexChannel типа IDuplexContextChannel, от- крывающее доступ к контексту через свойство Callbackinstance.
190 Глава 5. Операции Если для построения класса посредника для службы с контрактом обрат- ного вызова используется SvcUtil или Visual Studio 2005, сгенерированный класс объявляется производным от DuplexClientBase<T>, как показано в листин- ге 5.3. Листинг 5.3. Автоматически сгенерированный дуплексный посредник partial class MyContractClient : DuplexCl1entBase<IMyContгасt>.IMyContract {} public MyContractClient(InstanceContext callbackcontext) : oase(caIIbackContext) {} public MyContractClient(InstanceCoriext calIcackContext. string endpointName) . base(calIbackContext.endpointName) {} public MyContractClient(InstanceContext callbackcontext, Binding binding, EndpointAddress remoteAddress) : ba s e u:a11 back Context.oi nding.remot eAddress) {} //... public void DoSomething() Channel.DoSomethingf). } } Используя производный класс посредника, клиент конструирует экземпляр объекта обратного вызова, размещает его в контексте, создает посредника и об- ращается с вызовом к службе, таким образом передавая ссылку на конечную точку обратного вызова: class MyCalIback : IMyContractCalIback { public void OnCallbackO {•••} } IMyContractCalIback = new MyCallback(); InstanceContext content = new InstanceContext(calIback): MyContractClient proxy = new MyContractClient(context); proxy.DoSomething(); Обратите внимание: пока клиент ожидает обратные вызовы, он не может закрыть посредника, потому что это приведет к закрытию конечной точки об- ратного вызова и выдаче о!нибки на стороне службы при попытке обратного вызова. Достаточно часто клиент сам реализует контракт обратного вызова. В этом случае клиент обычно сохраняет посредника в переменной класса и закрывает его при уничтожении клиента, как показано в листинге 5.4.
Операции обратного вызова 191 Листинг 5.4. Реализация контракта обратного вызова клиентом class MyClient : IMyContractCallback.IDisposable ( MyContractCl ient m_Proxy: public void CallService() I InstanceContext context = new InstanceContext(this); m_Proxy = new MyContractClient(context); m Proxy.DoSomething(): ) public void OnCallbackO (...) public void Disposed ( m_Proxy.Closet); ) Как ни странно, сгенерированный посредник не использует вспомогатель- ные конструкторы DuplexCLientBase<T>, которым объект обратного вызова пере- дается напрямую, но вы можете переработать посредника вручную и включить в него соответствующую возможность, как показано в листинге 5.5. Листинг 5.5. Использование переработанного посредника на базе object partial class MyContractClient : DuplexClientBase<IMyContract>,IMyContract ( public MyContractClient(object callbackinstance) : base(callbacklnstance) {} // Другие конструкторы public void DoSomething() Channel .DoSomething(); ) ) class MyClient : IMyContractCallback.IDisposable ( MyContractClient m_Proxy; public void CallServiceO m_Proxy = new MyContractClient(this); m_Proxy. DoSomethi ng (); public void OnCallbackO (...) public void Disposed I m_Proxy.Closet):
192 Глава 5. Операции Инициирование обратного вызова на стороне службы Ссылка на конечную точку обратного вызова на стороне клиента передается с каждым вызовом, обращенным от клиента к службе, и является частью вход- ных сообщений. Класс OperationContext позволяет службе легко получить эту ссылку посредством параметризованного метода GetCallbackChanneL<T>(): public sealed class Operationcontext { public T GetCa11backChannel<T>(); } Что именно служба сделает со ссылкой, как она будет использовать ее — ос- тается полностью на ее усмотрение. Служба может извлечь ссылку на конечную точку обратного вызова из контекста операции и сохранить его для использова- ния в будущем или же использовать ее во время операции для обращения к кли- енту с обратным вызовом. Первый вариант продемонстрирован в листинге 5.6. Листинг 5.6. Сохранение ссылки на конечную точку обратного вызова [Servi ceBehav i or(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyService : IMyContract { static List<ISomeCallbackContract> m_Callbacks = new List<ISomeCallback<Contract>(); public void Dosomething() { ISomeCalIbackContract callback = OperationContext.Current. GetCallbackChannel<ISomeCallbackContract>(): if(m_Callbacks.Contains(callback) == false) { m_Ca11 backs.Add(ca 11 back): } } public static void CallClientsO { Action<ISomeCallbackContract> invoke = delegate(ISomeCal1backContract cal 1 back) { callback.OnCallbackO: }: m_Cal1 backs.ForEachtinvoke); } } Используя определения из листинга 5.1, служба использует статический шаблонный связанный список для хранения ссылок на интерфейсы типа ISome- CaLLbackContract. Поскольку служба не знает, какой клиент вызывает ее и посту- пил ли от клиента вызов или еще нет, при каждом вызове служба сначала про- веряет, присутствует ли ссылка на конечную точку обратного вызова в списке. Если ссылки в списке нет, служба включает ее. Класс службы также предостав- ляет статический метод CallCLients(). Сторона хоста может использовать его для обратного вызова клиентов: MyService.Cal 1 Cli ents():
Операции обратного вызова 193 В этом варианте вызывающая сторона использует для обратного вызова про- граммный поток на стороне хоста. Этот программный поток не связан с потока- ми, обрабатывающими входящий вызов. ПРИМЕЧАНИЕ------------------------------------------------------------------------ В листинге 5.6 (и других аналогичных примерах этой главы) обращения к списку обратных вызовов не синхронизируются. Разумеется, в коде реального приложения синхронизация должна присутст- вовать. Параллельная обработка (и особенно синхронизация доступа к общим ресурсам) рассматри- вается в главе 8. Реентерабельность обратного вызова Возможно, службе также потребуется обратиться по полученной ссылке обрат- ного вызова (или ее сохраненной копии) во время выполнения операции кон- тракта. Тем не менее по умолчанию такие вызовы запрещены. Это объясняется особенностями стандартной схемы управления параллельной обработкой. По умолчанию класс службы настроен на однопоточные обращения: экземпляр службы связывается с блокировкой, и в любой момент времени только один программный поток может захватить блокировку и обратиться к экземпляру службы. Обращение к клиенту во время вызова операции потребует блокиров- ки программного потока службы. Однако обработка ответного сообщения от клиента при возврате из обратного вызова потребует захвата той же блокиров- ки; возникает ситуация взаимной блокировки (deadlock). При этом служба мо- жет обращаться с обратными вызовами к другим клиентам или вызывать дру- гие службы. Взаимная блокировка возникает из-за обратного вызова клиента, от которого поступил вызов. Для предотвращения взаимной блокировки, если однопоточный экземпляр службы пытается обратиться к своему клиенту с обратным вызовом, WCF вы- дает исключение InvalidOperationException. Возможны три решения. Первое — настроить службу для многопоточного доступа, при котором она не будет свя- зываться с блокировкой; обратный вызов станет возможным, но на разработчи- ка службы будет возложено дополнительное бремя — необходимость синхрони- зации. Второе решение — настройка службы для повторного входа (реентера- бельности). В этом случае экземпляр службы по-прежнему связывается с бло- кировкой, и доступ к ней разрешен только в однопоточном режиме. Но когда служба обращается к клиенту с обратным вызовом, WCF сначала автоматиче- ски снимает блокировку. Глава 8 посвящена синхронизации и ее влиянию на модель программирования. А пока, если вашей службе потребуется обратиться к клиентам с обратным вызовом, установите для нее многопоточный или реен- терабельный режим при помощи свойства ConcurrencyMode атрибута ServiceBeha- vior: public enum ConcurrencyMode I Single. // По умолчанию Reentrant. Multiple ) [Attri buteUsage (Attri buteTargets .Class)] public sealed class ServiceBehaviorAttribute : ...
194 Глава 5. Операции { public ConcurrencyMode ConcurrencyMode {get:set;} //... } В листинге 5.7 представлена служба, настроенная в реентерабельном режи- ме. Во время выполнения операции служба обращается к контексту операции, получает ссылку на объект обратного вызова и обращается с вызовом. Управле- ние снова передается службе только после возврата из обратного вызова, при этом собственный программный поток службы должен заново захватить блоки- ровку. Листинг 5.7. Настройка реентерабельного режима для обратных вызовов [Servi ceContract(CallbackContract = typeof(IMyContractCa11 back))] interface IMyContract { [OperationContract] void DoSomethingO; } interface IMyContractCalIback { [OperationContract] void OnCallbackO: } [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)] class MyService ; IMyContract { public void DoSomethingO { IMyContractCalIback = Operationcontext.Current. GetCal1backChannel<IMyContractCal1back>(): cal1 back.OnCal1back(): } } Третье решение, позволяющее службе безопасно обращаться к клиенту с об- ратными вызовами, — определение односторонних операций в контракте обрат- ного вызова. В этом случае служба сможет использовать обратные вызовы даже в однопоточном режиме, потому что у нее не будет ответных сообщений, пре- тендующих на получение блокировки. Пример такой конфигурации представ- лен в листинге 5.8. Обратите внимание: по умолчанию служба работает в одно- поточном режиме. Листинг 5.8. Односторонние обратные вызовы разрешены по умолчанию [ServiceContract(CalIbackContract = typeof(IMyContractCalIback))] interface IMyContract { [OperationContract] void DoSomethingO; } interface IMyContractCalIback
Операции обратного вызова 195 [Operationcontract(IsOneWay e true)] void OnCallbackO; 1 class MyService : IMyContract public void DoSomething() f IMyContractCallback = Operationcontext .Current. GetCal1 backchannel<IMyContractCallback>(); callback.OnCallbackO; 1 Управление подключениями ' при обратном вызове Механизм обратного вызова не предоставляет ничего похожего на высокоуров- невые протоколы управления соединением между службой и конечной точкой обратного вызова. Разработчик должен сам представить некий протокол уровня приложения или единую схему управления жизненным циклом подключения. Как упоминалось ранее, служба может обращаться к клиенту с обратным вызо- вом только в том случае, если канал на стороне клиента остается открытым — обычно для этого клиент не закрывает посредника. Кроме того, поддержание посредника в открытом состоянии предотвращает уничтожение объекта обрат- ного вызова в ходе уборки мусора. Если служба хранит ссылку на конечную точку обратного вызова, а посредник закрывается на стороне клиента или исче- зает само клиентское приложение, при обратном вызове служба получит от ка- нала службы исключение ObjectDisposedException. Следовательно, клиенту сле- дует уведомить службу о том, что он более не намерен принимать обратные вызовы или о завершении клиентского приложения. Для этого в контракт служ- бы включается специальный метод Disconnect(). Поскольку с каждым вызовом метода передается ссылка на объект обратного вызова, в методе Disconnect() служба может удалить ссылку из своего внутреннего хранилища. Кроме того, по соображениям симметрии желательно добавить отдельный метод Connect(). Наличие метода Connect() позволит клиенту подключаться к служ- бе и отключаться несколько раз, а также четко обозначить время, в которое он ожидает получения обратных вызовов (только после вызова Connect()). Этот прием продемонстрирован в листинге 5.9. В обоих методах Connect() и Discon- nect() служба должна получить ссылку на объект обратного вызова. В Connect(), прежде чем добавлять ссылку в список, служба проверяет, что ее там еще нет (благодаря чему служба нормально выдерживает повторные вызовы Connect()). В Disconnect() служба проверяет, что ссылка присутствует в списке, и иници- ирует исключение в противном случае. Листинг 5.9. Явное управление подключениями обратного вызова [ServiceContract(CallbackContract = typeof(IMyContractCa11 back))] interface IMyContract ( продолжение &
196 Глава 5. Операции [Operationcontract] void DoSomethingO: [Operationcontract] void ConnectO; [Operationcontract] void Disconnect); ) interface IMyContractCallback { [Operationcontract] void OnCal1 back(): } [ServiceBehaviordnstanceContextMode = InstanceContextMode.PerCall)] class MyService : IMyContract { static List<IMyContractCallback> m_Callbacks = new List<IMyContractCallback>(); public void ConnectO { IMyContractCallback = OperationContext.Current. GetCal1 backchannel<IMyContractCal1back>(); if(m_Callbacks.Contains(callback) -- false) { m_Ca11 backs.Add(ca11 back): } } public void DisconnectO { IMyContractCallback = OperationContext.Current. GetCal1 backchannel<IMyContractCal1back>(): if(m_Callbacks.Contains(callback) »» true) { m_Ca11 backs.Remove(ca11 back): } else { throw new InvalidOperationExceptionCCannot find callback"); } } public static void Cal1C1tents) { Action<IMyContractCallback> invoke » delegate(IMyContractCal1 back cal 1 back) { callback.OnCal IbackO: }: m_Ca 11 backs. ForEach (1 nvoke): } public void DoSomethingO
Операции обратного вызова 197 Управление подключениями ирежим управления экземплярами Служба уровня вызова может использовать ссылку на объект обратного вызова вовремя вызова операции или сохранить ее в некотором глобальном хранили- ще-например, в статической переменной, как в приведенных ранее примерах. Причина понятна: любая переменная состояния экземпляра, в которой служба сохранит ссылку, все равно исчезнет после возвращения из операции. Соответ - ственно, использование метода-аналога Disconnect() особенно актуально для служб уровня вызова. Аналогичная необходимость существует и для синглетных служб. Срок жизни синглетного экземпляра не ограничен, поэтому через какое-то время в нем накопится множество ссылок на объекты обратного вызова; с течением времени многие ссылки устаревают, поскольку объекты перестают существо- вать. Наличие метода Disconnect() гарантирует, что синглетная служба сохранит связь только с актуальными, «живыми» клиентами. Интересно, что сеансовые службы могут обойтись вообще без метода Disconnect(), при условии, что ссылка на объект обратного вызова хранится в некоторой переменной экземпляра. Дело в том, что экземпляр службы будет автоматически уничтожен при завершении сеанса (когда клиент закроет по- средника или по тайм-ауту), поэтому хранение ссылки в рамках сеанса не пред- ставляет опасности — ссылка всегда заведомо действительна. Но если сеансо- вая служба сохраняет свою ссылку обратного вызова в глобальном хранилище для использования другими участниками на стороне хоста или за пределами се- анса, метод Disconnect() становится обязательным для явного удаления ссылки обратного вызова, потому что при вызове Dispose() ссылка недоступна. Наконец, пару Connect()-Disconnect() можно включить в сеансовую службу просто для удобства, чтобы клиент мог решить, когда следует начинать или за- канчивать прием обратных вызовов во время сеанса. Дуплексный посредник и безопасность типов Класс DuplexClientBase<T>, предоставляемый WCF, не обеспечивает сильной ти- пизации для используемого интерфейса обратного вызова. Компилятор позво- лит передать любой объект, даже недействительный интерфейс обратного вы- зова. Он даже разрешит использовать в качестве Т тип контракта службы, в котором контракт обратного вызова вообще нс определен. Во время выполне- ния экземпляр посредника будет успешно создан. Несовместимость проявится только при попытке его использования, с выдачей исключения InvalidOperation- Exception. Аналогично, InstanceContext тоже базируется на object, а компилятор не проверяет на действительность его экземпляр контракта обратного вызова. При передаче InstanceContext в параметре конструктора дуплексного посредни- ка отсутствует проверка времени компиляции, которая бы проверяла его соот- ветствие с экземпляром обратного вызова, ожидаемым дуплексным посредни- ком. Ошибка обнаруживается только при попытке использования посредника. Шаблоны позволяют до определенной степени компенсировать эти недочеты и обнаружить ошибку на стадии времени выполнения, при создании экземпля- ра посредника.
198 Глава 5. Операции Сначала определяется типизованный шаблонный класс InstanceContext<T> (листинг 5.10). Листинг 5.10. Класс InstanceContext<T> public class InstanceContext<T> { InstanceContext m_InstanceContext; public lnstanceContext(T implementation) { m_lnstanceContext = new InstanceContext(implementation); } public InstanceContext Context { get { return m_InstanceContext: } } public T Serviceinstance { get { return (T)m_InstanceContext.GetServiceInstance(); } } } Шаблоны позволяют предоставить типизованный доступ к объекту обратно- го вызова и сохранить информацию о правильном типе. Затем определяется новый типизованный шаблонный класс, производный от DuplexClientBase<T> (листинг 5.11). Листинг 5.11. Класс DuplexClientBase<T,C> // Т - контракт службы. С - контракт обратного вызова public abstract class DuplexCiientBase<T.C> : DuplexClientBase<T> where T : class protected DuplexC11entBase(InstanceContext<C> context) : base(context.Context) {} protected DuplexCl1entBase(InstanceContext<C> context. string endpointName) : ba se(context.Context.endpoi ntName) 0 protected DuplexClientBase(InstanceContext<C> context.Binding binding. EndpointAddress remoteAddress) : base(context.Context.binding.remoteAddress) {} protected DuplexClientBase(C callback) : base(callback) {} protected DuplexClientBase(C call back.string endpointName) : base(cal1 back.endpoi ntName) (}
Операции обратного вызова 199 protected DuplexCl ientBase(C cal 1 back.В inding binding. EndpointAddress remoteAddress) : base(ca11 back.bi ndi ng.remoteAddress) () /* Другие конструкторы */ static DuplexClientBase() VerifyCallbackO; internal static void VerifyCallbackO Type contractType = typeof(T): Type calIbackType = typeof(C): objects attributes = contractType.GetCustomAttributes( typeof(ServiceContractAttribute),false): if(attributes.Length != 1) throw new InvalidOperationExceptionC’Type of ” + contractType + " is not a service contract"): ServiceContractAttribute ServiceContractAttribute; ServiceContractAttribute = attributesLO]] as ServiceContractAttribute: if(calIbackType != serviceContractAttribute.CalIbackContract) throw new InvalidOperationExceptionC'Type of " + calIbackType + " is not configured as callback contract for " + contractType); ( ) 1 Класс DuplexClientBase<T,C> использует два параметра типа: Т — тип контракта службы, С — типа контракта обратного вызова. Конструкторы DuplexClientBase<T,C> могут получать либо «необработанный» экземпляр С, либо экземпляр InstanceContext<C>, инкапсулирующий экземпляр С. Это позволяет компилятору убедиться в использовании совместимых контекстов. Тем не ме- нее в C# 2.0 отсутствуют средства ограничения декларативных связей между Т и С. Обходное решение заключается в том, чтобы выполнить проверку во время выполнения перед использованием DuplexClientBase<T,C> и немедленно прервать использование недопустимого типа, пока он не успел причинить вред. Для это- го код проверки помещается в статический конструктор С#. Статический кон- структор DuplexClientBase<T,C> вызывает статический вспомогательный метод VerifyCallback(). Последний при помощи рефлексии убеждается в том, что тип Т помечен атрибутом ServiceContract. Затем он проверяет, что его тип, заданный для контракта обратного вызова, соответствует параметру типа С. Выдача ис- ключения в статическом конструкторе позволяет как можно ранее обнаружить ошибку на стадии выполнения. ПРИМЕЧАНИЕ-------------------------------------------------------------------------------- Обратите внимание на проверку контракта обратного вызова в статическом конструкторе. Этот при- ем применим к любым ограничениям, соблюдение которых не удается обеспечить на стадии компи- ляции, но при этом существует способ их программной проверки во время выполнения.
200 Глава 5. Операции Далее необходимо переработать сгенерированный класс посредника на сто- роне клиента и сделать его производным от типизованного класса DuplexClient- Base<T,C>: partial class MyContractClient : DuplexClientBase<IMyContract.IMyContractCallback>. IMyContract { public MyContractClient(InstanceContext<IMyContractCallback> context) : base(context) {} public MyContractClient(IMyContractCalIback callback) : base(calIback) {} /* Другие конструкторы */ public void DoSomething) { Channel.DoSomethlngt): } } Переработанному посреднику передается либо типизованный контекст эк- земпляра, либо непосредственно экземпляр объекта обратного вызова: // Клиентский код class MyClient : IMyContractCalIback {••} IMyContractCalIback callback = new MyClientO: MyContractClient proxyl = new MyContractClient(calIback); InstanceContex<IMyContractCallback> context = new InstanceContext<IMyContractCallback>(cal Iback); MyContractClient proxy2 = new MyContractClient(context); Дуплексная фабрика Наряду с классом ChannelFactory<T> WCF также предоставляет класс Duplex- ChannelFactory<T>, который может использоваться для создания дуплексных по- средников на программном уровне: public class DuplexChannelFactory<T> : ChannelFactory<T> { public DuplexChannel Factory(object callback); public DuplexChannel Factory(object call back.strint endpointName); public DuplexChannelFactory(InstanceContext context.string endpointName): public T CreateChanneKInstanceContext context); public static T CreateChannel(object calIback.strint endpointName); public static T CreateChannel(InstanceContext context. string endpointName): public static T CreateChannel(object calIback.Binding binding. Endpoi ntAddress endpoi ntAddress); public static T CreateChannel(InstanceContext context.Binding binding. Endpoi ntAddress endpointAddress); //... }
Операции обратного вызова 201 DuplexChannelFactory<T> используется так же, как его базовый класс Channel- Factory^, если не считать того, что его конструкторам передается либо экземп- ляр объекта обратного вызова, либо контекст обратного вызова. Также обрати- те внимание на использование типа object для экземпляра объекта обратного вызова и отсутствие безопасности типов. В листинге 5.12 представлен перера- ботанный класс DuplexChanneLFactory<T,C>, обеспечивающий безопасность типов на стадиях компиляции и выполнения. Листинг 5.12. Класс DuplexChannelFactory<T,C> public class DuplexChannelFactory<T.C> : DuplexChannelFactory<T> where T : class ( static DuplexChannel Factory () ( DuplexCl i entBase<T. C>. Ver i fyCa 11 back (); 1 public static T CreateChannel(C call back.string endpointName) ( return DuplexChannelFactory<T>.CreateChannel(call back,endpointName): 1 public static T CreateChannel (InstanceContextO context, string endpointName) ( return DuplexChannelFactory<T>.CreateChannel(context.Context. endpointName): ) public static T CreateChannel(c call back.Blnding binding. EndpointAddress endpointAddress) return DuplexChannelFactory<T>.CreateChannel(callback.binding. endpointAddpress): 1 public static T CreateChannel(InstanceContextO context.Binding binding. EndpointAddress endpointAddress) ( return DuplexChannelFactory<T>.CreateChannel(context.binding. endpointAddpress): I public Dupl exChannel Factory (C callback) : base(callback) (} public Dupl exChannel Factory(C callback,string endpointName) : base(cal1 back.endpointName) () public Dupl exChannel Factory (InstanceContextO context. string endpointName) : base(context.Context,endpointName) () //... 1 Пример использования DuplexChanneLFactory приведен в листинге 5.13; под- держка обратного вызова добавляется в статический вспомогательный класс InProcFactory, представленный в главе 1.
202 Глава 5. Операции Листинг 5.13. Включение поддержки обратного вызова в InProcFactory public static class InProcFactory { public static I CreateInstance<S.I,C>(C callback) where I : class where S : class,I { InstanceContextO context = new InstanceContextO(cal Iback); return CreateInstance<S,I.O(context): } public static I CreateInstance<S. I ,C>( InstanceContextO context) where I : class where S : class.I ( HostRecord hostRecord = GetHostRecord<S,I>(): return DuplexChannelFactory<I,C>.CreateChannel( context.NamedPIpeBInding.new EndpolntAddress(hostRecord.Address)): } //... } // Пример клиентского кода IMyContractCalIback callback = new MyClient(); IMyContract proxy = InProcFactory.Createlnstance <MyServ1ce.IMyContract.IMyContractCallback>(calIback); proxy.DoSomething(); InProcFactory.CloseProxy(proxy); Иерархия контрактов обратного вызова При проектировании контрактов обратного вызова действует одно любопытное ограничение. Контракт службы может назначить контракт обратного вызова только в том случае, если последний является субинтерфейсом всех контрактов обратного вызова, определенных собственными базовыми контрактами данного контракта службы. Например, следующее определение контрактов обратного вызова недействительно: Interface ICalIbackContractl {...} Interface ICallbackContract2 [ServiceContract(Cal 1backContract = typeof(ICa11backContractl))] interface IMyBaseContract {...} // Недопустимо [ServiceContract(Cal 1backContract = typeof(ICal1backContract2))] interface IMySubContract : IMyBaseContract IMySubContract не может назначить ICallbackContract2 в качестве контракта об- ратного вызова, потому что ICallbackContract2 не является субинтерфейсом ICall- backContractl, тогда как IMyBaseContract (базовый для IMySubContract) определяет
Операции обратного вызова 203 ICallbackContractl своим контрактом обратного вызова. Причина такого ограни- чения очевидна: если клиент передает ссылку на конечную точку реализации IMySubContract, эта ссылка должна соответствовать типу конечной точки, ожи- даемому IMyBaseContract. WCF проверяет иерархию контрактов обратного вызо- ва во время загрузки службы и выдает исключение InvalidOperationException в случае нарушения. Простейший способ выполнения этих ограничений — воспроизведение иерар- хии контракта службы в иерархии контрактов обратного вызова: interface ICallbackContractl (...) interface ICal 1 backContract2 : ICallbackContractl (...) [ServiceContract (Callbackcontract = typeof (ICallbackContractl))] interface IMyBaseContract (...) II Недопустимо [ServiceContract (Cal 1 backContract = typeof(ICallbackContract2))] interface IMySubContract : IMyBaseContract (...) Впрочем, возможен и другой вариант — использовать наследование от не- скольких интерфейсов одного контракта обратного вызова. В этом случае вам не придется имитировать иерархию контракта службы: interface ICallbackContractl (...) interface ICa 11 backContract2 (...) interface ICal 1 backContract3 : ICallbackContractl,ICal 1 backContract2 (.•I [ServiceContract (Cal 1 backContract = typeof (ICal 1 backContractl)) ] interface IMyBaseContractl (...) [ServiceContract (Cal 1 backContract = typeof (ICal 1 backCont ract2)) ] interface IMyBaseContract2 (...) [ServiceContract(CallbackContract = typeof (ICal lbackContract3))] interface IMySubContract : IMyBaseContractl. IMyBaseContract2 (...) Также следует учесть, что служба может реализовать собственный контракт обратного вызова: [ServiceContract(Cal 1 backContract - typeof(ICal 1 backContract))] interface IMyContract (...) [ServiceContract] interface IMyContractCallback (...) class MyService : IMyContract.IMyContractCallback (...)
204 Глава 5. Операции Служба даже может сохранить ссылку на саму себя в некотором хранилище ссылок (если она желает получать обратные вызовы, как если бы она была кли- ентом). Обратный вызов, порты и каналы При использовании привязок NetTcpBinding и NetNamedPipeBinding обратные вы- зовы поступают клиенту по входному каналу, поддерживаемому привязкой. Открывать для них новый порт или канал не нужно. При использовании WS- DualHttpBinding WCF поддерживает отдельный канал HTTP, предназначенный специально для обратных вызовов, потому что протокол HTTP сам по себе яв- ляется односторонним. Для канала обратного вызова WCF по умолчанию вы- бирает порт 80 и передает службе адрес обратного вызова, образованный из протокола HTTP, имени клиентского компьютера и порта 80. Использование порта 80 оправдано для служб, базирующихся в Интернете, но для интрасетевых служб оно не имеет особого смысла. Кроме того, если на клиентском компьютере работает IIS, порт 80 уже зарезервирован и клиент не сможет предоставить конечную точку обратного вызова. Вероятность того, что интрасетевое приложение будет вынуждено использовать WSDualHttpBinding, не- велика, но разработчики, занимающиеся интернет-базированными приложения- ми, часто устанавливают I1S на свои компьютеры; на стадии тестирования и от- ладки порт обратного вызова начинает конфликтовать с IIS. Назначение адреса обратного вызова К счастью, привязка WSDualHttpBinding предоставляет свойство ClientBaseAddress, позволяющее задать на стороне клиента другой URI обратного вызова: public class WSDualHttpBinding : Binding,... { public Uri ClientBaseAddress {get:set:} //... } А вот как базовый адрес настраивается в конфигурационном файле клиента: <system.servi ceModel> <client> <endpoint address = "http://1ocalhost:8008/MyService/" binding = "WSDualHttpBinding" bindingconfiguration = "CllentCalIback" contract = "IMyContract" /> </client> <binaings> <wsDualHttpBinding> <binding name = "Clientcallback" ClientBaseAddress - "http://localhost:8009/" /> </wsDua1HttpBi ndi ng> </bi ndi ngs> </system.servi ceModel>
Операции обратного вызова 205 Но поскольку порт обратного вызова не обязан быть известен службе зара- нее, на деле подойдет любой доступный порт. Соответственно, лучше указать любой доступный порт в клиентском базовом адресе на программном уровне. Статический вспомогательный класс WsDualProxyHelper, представленный в лис- тинге 5.13, помогает автоматизировать эту задачу. Листинг 5.14. Класс WsDualHttpProxyHelper public static class WsDualProxyHelper I public static void SetCHentBaseAddress<T>(DuplexC11entBase<T> proxy. Int port) where T : class ( WSDualHttpBinding binding = proxy.Endpoint.Binding as WSDualHttpBinding; Debug.Assert(bind1ng != null); blnding.CHentBaseAddress = new Uri("http;//localhost:" + port +"/"); 1 public static void SetCl1entBaseAddress<T>(DuplexC11entBase<T> proxy) where T : class 1 ock (typeof (WsDua 1 ProxyHel per)) ( Int portNumber = FlndPortO; SetCl 1 entBaseAddress (proxy. portNumber): proxy.Open(); } I internal static Int FindPortO ( IPEndPoint endPoInt = new IPEndPolnt(IPAddress.Any.O); us1ng(Socket socket = new Socket(AddressFamlly.InterNetwork. SocketType.Stream, ProtocolType.Tcp)) ( socket. Bl nd(endPoi nt); IPEndPoint local = (IPEndPoint)socket.LocalEndPoInt; return local.Port; } ) WsDualProxyHelper содержит две перегруженные версии метода SetClientBase- Address(). Первая версия просто получает экземпляр посредника и номер порта. Она проверяет, что посредник использует привязку WSDualHttpBinding, после че- го задает клиентский базовый адрес с использованием указанного порта. Вто- рая версия SetClientBaseAddress() автоматически выбирает доступный порт и вызывает первую версию с доступным портом. Чтобы избежать «гонки» с другими параллельными вызовами SetClientBaseAddress() в том же приклад- ном домене, на время поиска доступного порта и задания базового адреса ус- танавливается блокировка по самому типу. Учтите, что ситуация «гонки» все еще может возникнуть с другими процессами или прикладными доменами на том же компьютере.
206 Глава 5. Операции Класс WsDualProxyHelper прост в использовании: // Пример клиентского кода class MyCllent : IMyContractCallback IMyContractCallback callback = new MyClientO; InstanceContext context = new InstanceContext(calIback); MyContractClient proxy = new MyContractClient(context): WsDualProxyHelper.SetClientBaseAddressCproxy); У программного назначения адреса обратного вызова (в отличие от жестко- го кодирования в конфигурационном файле) есть и другое преимущество: оно позволяет запустить несколько клиентов на одном компьютере во время тести- рования. Декларативное назначение адреса обратного вызова Процесс можно дополнительно автоматизировать и задать порт обратного вы- зова на декларативном уровне с использованием специального атрибута. CallbackBaseAddressBehaviorAttribute — атрибут поведения контракта, а его дейст- вие распространяется только на конечные точки, использующие привязку WS- DualHttpBinding. CallbackBaseAddressBehaviorAttribute содержит одно целочислен- ное свойство с именем CallbackPort: LAttributeusage(AttributeTargers.Class)] public class CalIbackBaseAddressBehaviorAttribute ; Attribute.lEndpointBehavior { public mt CallbackPort (get:set:} } По умолчанию значение CallbackPort равно 80. Если оно не задано, примене- ние CallbackBaseAddressBehavior приводит к стандартному поведению WSDualHttp- Binding, поэтому следующие два определения эквивалентны: class MyCllent : IMyContractCallback {...} [CallbackBaseAddressBehavior] class MyCllent : IMyContractCallback {...} Помер порта обратного вызова явно задается в свойстве атрибута: [Cal 1backBaseAddressBehavlor(Cal1backPort « 8009)] class MyCllent : IMyContractCallback {...} Но если задать CallbackPort равным 0, CallbackBaseAddressBehavior автоматиче- ски выбирает для обратного вызова любой доступный порт: [Cal 1 backBaseAddressBehavlor(Са11backPort = 0)] class MyClient : IMyContractCallback {...} Код CallbackBaseAddressBehaviorAttribute приведен в листинге 5.15.
Операции обратного вызова 207 Листинг 5.15. CallbackBaseAddressBehaviorAttribute [Attributeusage(Attrl buteTargers. Cl ass) ] public class CallbackBaseAddressBehaviorAttribute : Attri bute.IEndpoi ntBehavi or I int m_Call backPort = 80; public int CallbacPort // Обращение к m_CallbackPort (get;set:} void IEndpolntBehavior.AddBindingParameters(ServiceEndpoint endpoint. BindingParameterCollection bindingparameters) 1 if(Cal IbackPort =- 80) { return; } 1 ock (typeof(WsOua1 ProxyHelper)) I if(CalIbackPort — 0) ( CallbackPort = WsDualProxyHelper.FindPortO; } WSDualHttpBinding binding = endpoint.Binding as WSDualHttpBinding; if(binding != null) { binding.ClientBaseAddress = new Uri( "http://localhost:” + CallbackPort + ’7"): } } // Пустые методы lEndpointBehavior ) Атрибут CallbackBaseAddressBehaviorAttribute позволяет перехватить (на сто- роне клиента или службы) информацию о конфигурации конечной точки. Ат- рибут поддерживает интерфейс IContractBehavior: public interface lEndpointBehavior I void AddBindingParameters(ServiceEndpoint endpoint. BindingParameterCollection bindingparameters); //... ) WCF вызывает метод AddBindingParameters() на стороне клиента непосредст- венно перед первым использованием посредника на стороне службы, что позво- ляет атрибуту настроить привязку, используемую для обратного вызова. Add- BindingParameters() проверяет значение CallbackPort. Если оно равно 80, ничего не происходит. Если оно равно 0, AddBindingParameters() находит свободный порт и присваивает его CallbackPort. Затем AddBindingParameters() проверяет при- вязку, используемую для вызова службы. Если это WSDualHttpBinding, AddBin- dingParameters() задает клиентский базовый адрес с использованием порта об- ратного вызова.
208 Глава 5. Операции ВНИМАНИЕ --------------------------------------------------------------------------- При использовании CallbackBaseAddressBehaviorAttribute возможна ситуация «гонки» с другим объектом обратного вызова, захватывающим тот же порт — даже в границах одного прикладного домена. События Базовый механизм обратного вызова WCF не делает никаких предположений относительно характера взаимодействия между клиентом и службой. Они мо- гут быть равноправными участниками взаимодействия, каждый из которых от- правляет и получает вызовы от другого. Тем не менее в каноническом варианте дуплексные обратные вызовы приме- няются для реализации событий. События предназначены для оповещения клиента или клиентов о том, что происходит на стороне службы. Событие мо- жет быть прямым результатом клиентского вызова или же произойти при вы- полнении некоторого условия, отслеживаемого службой. Служба, инициирую- щая событие, называется издателем1 (publisher), а клиент, получающий собы- тие, — подписчиком (subscriber). Поддержка событий играет важную роль во многих приложениях (рис. 5.2). Рис. 5.2. Служба-издатель может выдавать события для нескольких клиентов-подписчиков Хотя в WCF события представляют собой не что иное, как операции обрат- ного вызова, по своей природе они обычно подразумевают более свободную 1 Также встречается перевод «сервер публикаций» — Примеч. перев.
События 209 связь между издателем и подписчиком (по сравнению со связью между клиен- том и службой). Как правило, одно событие публикуется службой для несколь- ких клиентов-подписчиков. Часто издателя не интересует, в каком порядке со- бытие будет получено подписчиками и какие ошибки произойдут при обработке события подписчиками. Издателю известно только одно: что событие должно быть доставлено подписчикам. Даже если у них возникнут проблемы с событи- ем, служба все равно с этим сделать ничего не сможет. Кроме того, служба не собирается принимать результаты, возвращаемые подписчиками. Соответствен- но, операции обработки событий возвращают void, не могут возвращать инфор- мации в параметрах и должны помечаться как односторонние. Я также рекомен- дую выделить события в отдельный контракт обратного вызова и не смешивать их с обычными обратными вызовами того же контракта: interface IMyEvents [Operationcontract(IsOneWay - true)] void OnEventlO; [Operationcontract(IsOneWay - true)] void 0nEvent2(int number); [Operationcontract(IsOneWay - true)] void 0nEvent3(int number.string text); ) На стороне подписчика, даже при использовании односторонних операций обратного вызова, реализация методов обработки событий должна иметь мини- мальную длину. Дело в том, что при большом количестве публикуемых собы- тий очередь событий подписчика может оказаться переполненной, и работа из- дателя будет заблокирована. Из-за блокировки издателя события не смогут своевременно достичь других подписчиков. Издатель может включить в свой контракт специализированные операции, при помощи которых клиенты смогут явно заявить о желании подписаться или прекратить подписку на события. Если издатель поддерживает несколько типов событий, он также может пре- доставить возможность подписчикам явно указать, на какие события они жела- ют подписаться (или прекратить подписку). Как служба организует внутреннее управление списком подписчиков, ка- кими критериями она руководствуется — все это относится к подробностям реализации на стороне службы, которые никак не должны отражаться на кли- ентах. Для ведения списка подписчиков и оформления подписки издатель даже может использовать делегатов .NET. В листинге 5.16 продемонстрирована эта методика, а также другие проектировочные методики, упоминавшиеся ранее. Листинг 5.16. Управление событиями с использованием делегатов enum EventType I Event1 = 1. Event? = 2. Event3 = 4. продолжение &
210 Глава 5. Операции All Events = Eventl|Event21Event3 } [ServiceContract(CallbackContract - typeof(IMyEvents))] interface IMyContract { [OperationContract] void DoSomethingO; [OperationContract] void Subscr1be(EventType mask): [OperationContract] void UnsubscribeCEventType mask): } [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCai 1)] class MyPublisher : IMyContract { static GenerlcEventHandler m_Eventl - delegated; static Gener1cEventHandler<1nt> m_Event2 - delegate^}; static Gener1cEventHandler<1nt.str1ng> m_Event3 = delegate^}; public void Subscr1be(EventType mask) { IMyEvents subscriber = Operationcontext.Current. GetCa11 backchannel<IMyEvents>(); If((mask && EventType.Eventl) == EventType.Event1) { m_Eventl += subscrlber.OnEventl; } 1f((mask && EventType.Event?) — EventType.Event?) { m_Event2 += subscr1ber.0nEvent2; ) 1f((mask && EventType.Event3) — EventType.Event3) { m_Event3 +- subscriber.OnEventS: } ) public void UnsubscribeCEventType mask) { // Аналогично Subscribe!). но с операцией -= } public static void F1reEvent(EventType eventType) { switch(eventType) { case EventType.Eventl: { m_Eventl(); return; } case EventType.Event?: { m_Event2(42); return; }
case EventType.Event3: { m_Event3(42."Hello"); return; default; ( throw new Inval idOperatior,Except ion( "Unknown event tyoe"). } public void DoSomethingO ((...) Контракт службы IMyContract определяет методы Subscribe() и Unsubscribe(). Эти методы получают значение перечисляемого типа EventType, отдельные поля которого равны последовательным степеням 2. Это позволяет клиенту-подпис- чику объединить значения в маску, обозначающую типы событии, па которую он хочет подписаться (или прекратить подписку). Например, чтобы подписать- ся на Eventl и Event3, но не на Event2, подписчик использует вызов следующего вида: class MySubscrlber IMyEvents I void OnEventn) (...) void 0nEvent2( int number) (...) void 0nEvent3(int number.string text) (...) 1 IMyEvents subscriber = new MySubscrlber; Instancecontext context = new InstanceContext(subscrwer). MyContractCilent proxy = new MyContractCllent(context): proxy.Subscr 1 be(EventType. Eventl[EventType. Event3): Во внутренней реализации MyPublisher поддерживает трех статических деле- гатов, каждый из которых соответствует типу события. Все делегаты относятся к обобщенному типу делегата GenericDelegate: public delegate void GenericEventHandler(); public delegate void GenericEventHandler<T>(T t): public delegate void GenericEventHandler<T.U>(T t.U u); public delegate void GenericEventHanoler<l ,U.V>(T t.U u.V v); public delegate void GenericEventHandier<T.U.V,W>(T t.U u.V v.W w); public delegate void GenericEventHandler<T,U.V.W.X>(1 t.U u.V v.W w.X x); public delegate void GenericEventHandler<T.U.V.W.X.Y>(T t.U u.V v.W w.X x.Y y); GenericDelegate позволяет выразить буквально любую сигнатуру обработки события. Оба метода, Subscribe() и Unsubscribe(), проверяют переданное значение EventType и добавляют или удаляют ссылку обратного вызова подписчика в со- ответствующем делегате. Для инициирования событий в MyPublisher входит статический метод FireEvent(). При вызове он получает инициируемое событие и вызывает соответствующего делегата.
212 Глава 5. Операции Еще раз подчеркну: тот факт, что служба Му Publisher использует делегатов - не более чем подробность реализации, упрощающая поиск событий. Например, служба также могла бы использовать связанный список, но это привело бы к некоторому усложнению кода. ПРИМЕЧАНИЕ ---------------------------------------------------------- В приложении Б приведена заготовка для поддержки более эффективной схемы работы с собы- тиями. Потоковая передача При обмене сообщениями между клиентом и службой такие сообщения по умолчанию буферизуются на стороне получателя и доставляются только после приема всего сообщения. Сказанное относится как к отправке сообщения кли- ентом службе, так и возврате сообщений от службы клиенту. Когда клиент об- ращается с вызовом к службе, служба активизируется только после получения полного сообщения. Блокировка клиента снимается тогда, когда будет получе- но полное сообщение с результатами вызова. Для достаточно малых сообщений такая схема обмена упрощает модель программирования, потому что задержка, обусловленная приемом сообщений, обычно пренебрежимо мала по сравнению с продолжительностью обработки сообщения. Но когда мы имеем дело с боль- шими сообщениями (например, с мультимедийным содержимым, большими файлами или блоками данных), блокировка до приема полного сообщения мо- жет оказаться непрактичной. В подобных случаях WCF позволяет принимаю- щей стороне (будь то клиент или служба) приступить к обработке данных, пока сообщение все еще принимается каналом. Это называется режимом потоковой передачи (streaming transfer). При больших объемах передаваемых данных по- токовая передача улучшает пропускную способность и скорость отклика, пото- му что ни принимающая, ни отправляющая сторона не блокируется при от- правке или приеме сообщения. Потоки ввода/вывода Для потоковой передачи сообщений в WCF необходимо использовать класс .NET Stream. По сути контрактные операции, используемые для потоковой пе- редачи, выглядят как обычные методы ввода/вывода. Класс Stream является ба- зовым для всех потоков ввода/вывода в .NET (таких, как FileStream, Network- Stream и MemoryStream) и позволяет организовать потоковую передачу данных от всех перечисленных источников ввода/вывода. Все, что потребуется от вас - вернуть или получить Stream в параметре операции, как показано в листин- ге 5.17. Листинг 5.17. Потоковые операции [ServiceContract] interface IMyContract { [Operationcontract]
Streaa StreamReplylO; [OperationContract] void StreamReply2(out Stream stream): [OperationContract] void StreamRequest(Stream stream): [OperationContract(IsOneWay - true)] void OneWaySt ream (Stream stream): ) Обратите внимание: в параметре операции может использоваться только аб- страктный класс Stream или конкретный сериализуемый субкласс — такой, как MemoryStream. Такие субклассы, как FileStream, не сериализуются и вместо них придется использовать базовый класс Stream. WCF позволяет службам организовать потоковую обработку ответов, запро- сов или ответов и запросов, с применением односторонней потоковой пере- дачи. Чтобы передать в потоковом режиме ответ, либо верните Stream из опера- ции: [OperationContract] Streaa GetStreamK): либо передайте поток в выходном параметре: [OperationContract] void GetStream2(out Stream stream); Для потокового приема запроса передайте Stream в параметре метода: [OperationContract] void SetStreaml(Stream stream): Наконец, потоковая передача запроса возможна даже для односторонних операций: //Односторонняя потоковая передача [OperationContract(IsOneWay - true)] void SetStream2(Stream stream): Потоковая передача и привязка Только привязки BasicHttpBinding, NetTcpBinding и NetNamedPipeBinding поддер- живают потоковую передачу. У всех перечисленных привязок потоковый ре- жим отключен по умолчанию, а сообщение буферизуется даже при использова- нии Stream. Чтобы включить потоковый режим, задайте соответствующее зна- чение свойству TransferMode; например, для BasicHttpBinding: public enum TransferMode ( Buffered. // По умолчанию Streamed. StreamedRequest. StreamedRes ponse ) public class BasicHttpBinding : Binding....
214 Глава 5. Операции { public TransferMode TransferMode {get;set:} //... } TransferMode.Streamed поддерживает все варианты потоковой передачи. Это единственный режим, в котором поддерживаются все операции из листин- га 5.17. Но если контракт поддерживает определенную разновидность потоко- вой пересылки — например, потоковый ответ: [ServiceContract] interface IMyContract { // Потоковый ответ [Operationcontract] Stream GetStreamlC): [Operationcontract] int MyMethodO: } вы можете включить режим буферизованных запросов и потоковых ответов, выбрав значение TransferMode.StreamedResponse. Требуемый потоковый режим должен быть настроен на стороне клиента и/или службы: <configuration> <system.servi ceModel> <client> <endpoi nt binding = "basicHttpBinding" bindingconfiguration - "StreamedHTTP" /> </client> <bindings> <basicHttpBinding> <binding name = "StreamedHTTP" transferMode » "Streamed"> </binding> </basicHttpBinding> </bindings> </system.servi ceModel> </configuration> Потоковая передача и транспорт Важно понимать, что потоковая передача WCF — не более чем удобство про- граммной модели. Нижележащий транспорт (например, HTTP) не использует потоковый режим, а максимальный размер сообщения по умолчанию устанав- ливается равным 64 Кбайт. Это может создать проблемы с данными, для кото- рых обычно применяется потоковая передача, потому что потоковые сообщения обычно бывают очень большими (собственно, именно это и создает необхо- димость в потоковой передаче). Вероятно, вам придется увеличить максималь- ный размер сообщения на стороне получателя, чтобы сделать возможной пере-
Потоковая передача 215 дачу больших сообщений — для этого нужный максимальный размер задается в свойстве MaxReceivedMessageSize: public class BasicHttpBinding : Binding.. . public long MaxReceivedMessageSize (get: set:} //... Обычно подобные параметры следует задавать в конфигурационном файле, а не па программном уровне, поскольку размер сообщения часто зависит от конкретного места развертывания: <Dindings> <basicHttpBi ndi ng> <binding name = "StreamedHTTP" transferMode = ’Streamed" maxReceivedMessageSize = "120000"> Управление потоковой передачей Когда клиент передаст службе потоковый запрос, служба может прочитать дан- ные из потока уже после того, как клиент прекратит существование. Клиент не может узнать о том, что служба завершила работу с потоком. Соответственно, клиент не должен закрывать поток — WCF автоматически закроет поток на стороне клиента после того, как служба обработает его. Аналогичная проблема возникает при взаимодействии клиента с потоковым ответом. Поток был создан на стороне службы, но служба нс узнает, когда кли- ент завершит работу с потоком. WCF понятия не имеет, что клиент делает с по- током и поэтому помочь не сможет. Клиент всегда несет ответственность за за- крытие ответных потоков. ПРИМЕЧАНИЕ------------------------------------------------------------------------ Потоковая передача делает невозможным применение безопасности передачи на уровне сообще- ний. Тема безопасности более подробно рассматривается в главе 10. При потоковой передаче с привязками TCP также нельзя включить надежную доставку сообщений. Применение потоковой передачи имеет ряд дополнительных следствий. Прежде всего необходимо синхронизировать доступ к потоковому содержимо- му: например, открыв файловый поток в режиме «только для чтения», чтобы другие стороны могли к нему обращаться, или в монопольном режиме, чтобы запретить доступ к нему (если это необходимо). Кроме того, потоковая переда- ча не может использоваться с сеансовыми службами сеанс подразумевает же- стко фиксированную последовательность выполнения и четкую демаркацию, в отличие от потоковой передачи, которая может непрерывно продолжаться в течение долгого периода времени.
Сбои и исключения Любая операция службы может в произвольный момент времени столкнуться с непредвиденной ошибкой. Вопрос в том, как сообщить клиенту об этой ошиб- ке (и нужно ли это делать). Такие концепции, как исключения и механизмы обработки исключений, относятся к специфике конкретных технологий и не должны выходить за границы службы. Кроме того, обычно обработка ошибок является локальной подробностью реализации, которая не должна отражаться на клиенте — отчасти потому, что клиента могут не интересовать подробности ошибки (достаточно знать, что произошло что-то нежелательное), но чаще при- чина кроется в другом. В хорошо спроектированном приложении служба ин- капсулируется так, что клиент все равно не может осмысленно среагировать на ошибку. Хорошо спроектированная служба должна быть как можно более авто- номной и не зависящей от способности клиента обработать ошибку и вернуться к нормальной работе. Все, что выходит за пределы простого оповещения об ошибке, должно стать частью контрактного взаимодействия между клиентом и службой. В этой главе описано лишь то, как служба и клиент должны обраба- тывать такие объявленные сбои и как можно расширить и усовершенствовать базовый механизм. Ошибки и исключения В традиционной модели программирования .NET необработанное исключение немедленно завершает процесс, в котором оно произошло. В WCF эта модель поведения не используется. Если вызов службы от одного из клиентов приво- дит к исключению, это не должно нарушить работоспособность хостового про- цесса. Другие клиенты, обращающиеся к службе, и другие службы, находящие- ся под управлением того же процесса, страдать не должны. В результате, когда необработанное исключение выходит из области видимости службы, диспетчер перехватывает и обрабатывает его, возвращая в сериализованном виде в сооб- щении клиенту. Когда возвращаемое сообщение попадает к посреднику, по- следний инициирует исключение на стороне клиента. Вообще говоря, ошибки, с которыми может столкнуться клиент при попытке обращения к службе, делятся на три типа. Первый тип — коммуникационные
Сбои и исключения 217 ошибки (недоступность сети, неверный адрес, хостовой процесс не запущен нт. д. О коммуникационных ошибках на стороне клиента сигнализирует ис- ключение CommunicationException. Ошибки второго типа связаны с состоянием посредника и каналов - напри- мер, попытка обращения к уже закрытому посреднику, приводящая к исключе- нию ObjectDisposed Exception, или несоответствие между контрактом и уровнем безопасности привязки. Ошибки третьего типа возникают при вызове службы: либо сама служба инициирует исключение, либо оно возникает в результате обращения службы к другому объекту или ресурсу. Именно таким ошибкам посвящена настоящая глава. В интересах инкапсуляции и логической изоляции все исключения, иниции- рованные на стороне службы, по умолчанию всегда достигают клиента в виде FaultException: public class FaultException : CommunicationException (...) Делая все исключения служб неотличимыми друг от друга, WCF отделяет клиента от службы. Чем меньше клиент знает о том, что произошло на стороне службы, тем полнее логическое разделение такого взаимодействия. Исключения и управление экземплярами Хотя WCF и не уничтожает хостовой процесс, когда экземпляр службы сталки- вается с исключением, ошибка может повлиять на работу экземпляра службы и на способность клиента продолжить использование посредника (а вернее, ка- нала), связывающего его со службой. Воздействие, оказываемое исключением на клиента и на экземпляр службы, зависит от режима управления экземплярами. Службы уровня вызова и исключения Если при вызове происходит исключение, после исключения экземпляр служ- бы уничтожается, а посредник инициирует исключение FaultException на сторо- не клиента. По умолчанию все исключения, инициируемые службами (кроме классов, производных от FaultException), приводят к отказу канала. Даже если клиент перехватит исключение, он не сможет выдавать последующие вызовы, потому что это приведет к выдаче исключения CommunicationObjectFaultedExcep- tion. Клиент может только закрыть посредника. Сеансовые службы и исключения При использовании любой из сеансовых привязок WCF все исключения (кро- ме классов, производных от FaultException) по умолчанию завершают сеанс. WCF уничтожает экземпляр, а клиент получает исключение FaultException. Даже если клиент перехватит исключение, он не сможет выдавать последующие вы- зовы, потому что это приведет к выдаче исключения CommunicationObjectFaulted- Exception. Единственное, что может сделать клиент, — закрыть посредника; по- сле того как экземпляр службы, участвующей в сеансе, столкнется с ошибкой, сеанс не должен далее использоваться.
218 Глава 6. Сбои и исключения | Синглетные службы и исключения Если исключение происходит при вызове синглетной службы, синглетный эк- земпляр не уничтожается и продолжает работать. По умолчанию все исключе- ния (кроме классов, производных от FaultException) приводят к отказу канала, и клиент не может выдавать последующие вызовы, кроме закрытия посредника. Если у клиента существует сеанс с синглетной службой, этот сеанс завершается. Сбои Основная проблема с исключениями заключается в том, что они привязаны к конкретной технологии и не могут выходить за границы службы. Для обеспе- чения нормального взаимодействия необходимо найти способ отображения специфических исключений на некую нейтральную информацию об ошиб- ках. Такое представление называется сбоями (faults). Сбои основаны на про- мышленном стандарте, независимом от исключений, относящимся к конкретной технологии (например, исключениям CLR. Java или C++). Чтобы инициировать сбой, служба не может инициировать «простое» исключение CLR. Вместо этого она должна инициировать экземпляр класса FaultException<T> (листинг 6.1). Листинг 6.1. Класс FaultException<Т> LSerializablej // Другие атрибуты public class FaultException : Common i cat 1 or,Except ion J public Fan It Exceptюп(): public FaultException(string reason); public faultException(FaultReason reason); public vn+ual MessageFault CreateMessageFault(); I /. . j [Serializablel public class FaultExceptюп<Г> ; FaultException t public FaultException(T detail); public FaultException(T detail.string reason); public FauitException(Г detai 1.FauItReason reason). //... } FaultException<T> — специализация FaultException, поэтому любой клиент, за- программированный на работу с FaultException, сможет работать и с FaultExcep- tion<T>. Параметр типа Т у FaultException<T> содержит информацию об ошибке. Это может быть любой тип, не обязательно производный от Exception. Единственное ограничение заключается в том, что тип должен быть сериализуемым или яв- ляться контрактом данных. В листинге 6.2 представлена простая служба-калькулятор, реализация кото- рой выдает исключение FaultException<DivideByZeroException> при делении на ноль в операции Divide().
Листинг 6.2. Выдача FaultException<T> [ServiceContract] interface iCalculator I [OperationContract] double Divide(double numberl,double number?); //... I class Calculator : ICalculator I public double D1vide(double numberl.double number?) if (number? == 0) ( DivideByZeroException exception = new DivideByZeroException(); throw new FaultException<DivideByZeroException>(exception); return numberl / number?; ) //... I Вместо FaultException<DivideByZeroException> в параметре типа служба также могла бы передать класс, не являющийся производным от Exception: throw new Faul tExcepti on<doubl e>(); И все же, на мой взгляд, использование типа, производного от Exception, луч- ше отвечает традиционным принципам программирования .NET и делает код более удобным. Кроме того, это делает возможной доставку исключений (см. далее). Параметр reason, передаваемый конструктору FaultException<T>, содержит крат- кое описание исключения. В нем может передаваться как обычная строка: DivideByZeroException exception = new DivideByZeroException(): throw new Faul tExcepti on<DivideByZeroException>( except ion. "number? is 0"); так и объект FaultReason, который может пригодиться при локализации. Контракты сбоев По умолчанию любое исключение, инициированное службой, достигает клиен- та в виде FaultException. Дело в том, что вся информация, которой служба делит- ся с клиентом (кроме коммуникационных ошибок), должна быть частью кон- трактного поведения службы. С этой целью WCF предоставляет контракты сбоев - с их помощью служба перечисляет типы ошибок, которые она может инициировать. Идея заключается в том, что эти типы ошибок должны соответ- ствовать параметрам типов, используемым в FaultException<T>; по контракту сбоев клиент WCF может отличить контрактные ошибки от других. Служба определяет свои контракты сбоев при помощи атрибута FaultContract- Attribute:
220 Глава 6. Сбои и исключения LAttrbutellsage(AttributeTargets.Method.Al 1 owMultiplе « true. Inherited = false)] public sealed class FaultContractAttrlbute : Attribute { public FaultContractAttr1bute(Type detailType); //... Атрибут FaultContract применяется непосредственно к контрактным операци- ям, и в нем указывается тип детализации ошибки, как показано в листинге 6.3. Листинг 6.3. Определение контракта сбоя [ServiceContract] interface {Calculator { [Operationcontract] double Add(double number1.double number?); [Operationcontract] [FaultContract(typeof(DivideByZeroException))J double DIvide(double number1.double number?): //... Действие атрибута FaultContract ограничивается тем методом, который им помечен. Только этот метод может инициировать данный сбой, который в ко- нечном итоге будет передан клиенту. Кроме того, если операция инициирует исключение, отсутствующее в контракте, оно достигает клиента в виде простого FaultException. Для продвижения исходного исключения служба должна указать точно такой же тип, как указано в контракте сбоев. Например, для следующего определения контракта сбоев: [F a u1tContract(typeof(DIvldeByZeroExcept1 on))] служба должна инициировать FaultException<DivideByZeroException>. Чтобы ис- ключение было успешно доставлено, служба не может даже инициировать ис- ключение с подклассом детализирующего типа из контракта сбоев: [ServiceContract] interface IMyContract { [Operationcontract] [FaultCont ra c t(typeo f(Excepti on))] void MyMethodO; } class MyService : IMyContract { public void MyMethodO { //He соответствует контракту! throw new FaultExcept1on<D1v1deByZeroException>(new DIvldeByZeroExceptIonО);
Контракты сбоев 221 Атрибут FaultContract допускает многократное назначение, что позволяет за- дать для одной операции несколько контрактов сбоев: [ServiceContract] interface ICalculator [OperationContract] [Faul tCont ract (ty peof (I nval 1 dOperat 1 onExcept 1 on)) ] [Faul tContract (typeof (stri ng)) ] double Add (double numberl .double number2); [OperationContract] [FaultContract (typeof (Di v i deByZeroExcept i on)) ] double Divide(double number1.number?): //... 1 Это позволяет службе инициировать любое из исключений, упомянутых в контрактах, и это исключение будет успешно доставлено клиенту. ВНИМАНИЕ------------------------------------------------------------------------------------------ Контракты сбоев не могут определяться для односторонних операций, потому что односторонняя операция по определению не возвращает никаких результатов: //Недопустимое определение [ServiceContract] interface IMyContract I [OperationContract(IsOneWay = true)] [FaultContract(...)] void MyMethodt): Подобные попытки приведут лишь к выдаче исключения InvalidOperationException во время загруз- ки службы. Обработка сбоев Контракты сбоев публикуются наряду с другими метаданными службы. При импортировании метаданных клиентом WCF определение контракта содержит контракты сбоев, а также определения детализирующих типов, включая соот- ветствующий контракт данных. Последнее обстоятельство важно, если детали- зирующий тип представляет собой некоторый пользовательский тип исключе- ния со специализированными полями. Предполагается, что клиент будет перехватывать и обрабатывать импорти- рованные типы сбоев. Например, клиент, написанный для контракта из листин- га 6.3, может перехватить сбой FaultException<DivideByZeroException>: Calculatorclient proxy = new Calculator^ient(): try ( proxy. Di vide( 2.0): proxy.CloseO: 1 catch(FaultExcepti on<DivideByZeroException> exception)
222 Глава 6. Сбои и исключения (...) cdtcruCommuniCq!1onixception exception) i Обратите внимание на возможность возникновения коммуникационных ис- ключений. Клиент может решить, что все не-коммуникационные исключения со сторо- ны службы должны обрабатываться одинаково; для этого он обрабатывает толь- ко базовое исключение FaultException: Ca 'culator Client pruxy - new Ca IculatorQi lento: try Df’ox3 Div ide (2. O' pre*у (i)• catch;FaultException exception) catchcCommunicatlonException exception) ПРИМЕЧАНИЕ ----------------------------------------------------------------------- Возможна довольно экзотическая ситуация, когда разработчик клиента вручную изменяет опреде- ление импортированного контракта и удаляет из него контракт сбоев на стороне клиента. В этом случае, когда служба инициирует исключение, присутствующее в контракте сбоев на стороне служ- бы, исключение проявляется на стороне клиента как FaultException, а не как контрактный сбой. Когда служба инициирует исключение, указанное в контракте сбоев на сто- роне службы, исключение не приводит к отказу канала связи. Клиент может пе- рехватить исключение и продолжить пользоваться посредником или же за- крыть посредника. Неизвестные сбои Класс FaultException<T> является производным от FaultException. Служба (или любой используемый ей нисходящий объект) может инициировать исключение FaultException напрямую: throw new FaultException("Some Reason"): FaultException специальный тип для исключений, которые я называю неиз- вестными сбоями (unknown faults). Неизвестный сбой доставляется клиенту в виде FaultException, не приводит к отказу канала связи, и клиент может про- должить пользоваться посредником так, как если бы исключение входило в кон- тракт сбоев. ПРИМЕЧАНИЕ --------------------------------------------------------------- Обратите внимание: любое исключение FaultException<Т>, инициированное службой, всегда дос- тигает клиента в виде FaultException<Т> или FaultException. Если контракт сбоев отсутствует (или если T не входит в контракт), то любые исключения FaultException<Т> и FaultException, иницииро- ванные службой, достигают клиента в виде FaultException.
Контракты сбоев 223 Свойству Message объекта исключения на стороне клиента задается значение параметра конструирования reason объекта FaultException. В основном FaultEx- ception используется нисходящими объектами, не располагающими информа- цией о контрактах сбоев тех служб, от которых поступил вызов. Чтобы избе- жать логической привязки к службе верхнего уровня, такие объекты могут инициировать FaultException, если они хотят избежать отказа канала связи, или предоставить клиенту возможность обработать исключение отдельно от других коммуникационных ошибок. Отладка сбоев Уже развернутая служба должна быть по возможности отделена от клиентов, ее контракты сбоев должны быть сведены к абсолютному минимуму, а клиенты должны получать как можно меньше информации об исходной ошибке. Однако в процессе тестирования и отладки очень полезно передавать все исключения в информации, возвращаемой клиенту. Это позволит разработчику применять тестовых клиентов и отладчики для анализа источника ошибки (вместо того, чтобы иметь дело с универсальным, но не несущим полезной информации FaultException). Для этой цели используется класс Exception Detail, определяемый следующим образом: [DataContract] public class ExceptionDetail public Except!onDetail(Exception exception): [DataMember] public string HelpLink {getprivate set;} [DataMember] public ExceptionDetail InnerException {get private set:} [DataMember] public string Message {get;private set:} [DataMember] public string StackTrace {getprivate set;} [DataMember] public string Type {getprivate set;} Сначала вы создаете экземпляр Exception Detail и инициализируете его ис- ключением, которое должно быть доставлено клиенту. Затем инициируется объект FaultException<ExceptionDetail>, при конструировании которого указыва- ется экземпляр ExceptionDetail и сообщение исходного исключения. Последова- тельность действий показана в листинге 6.4.
224 Глава 6. Сбои и исключения Листинг 6.4. Включение исключения [ServiceContract] Interface IMyContract { [OperationContract] void MethodWithError(); } class MyServic : IMyContract ( public void MethodWithError() { InvalldOperatlonExceptlon exception - new InvalldOperatlonExceptlon("Some error"); ExceptionDeta11 detail e new ExceptionDeta11(exception); throw new FaultException<ExceptionDeta11>(detail.exception.Message); } } В этом случае клиент сможет получить информацию о типе и сообщении ис- ходного исключения. Объект сбоя на стороне клиента содержит свойство Detail. Туре, в котором хранится имя исходного исключения службы, и свойство Message с сообщением исходного исключения. В листинге 6.5 приведен клиент- ский код обработки исключения, инициированного в листинге 6.4. Листинг 6.5. Обработка вложенного исключения MyContractCllent proxy = new MyContractClient(endpointName): try { proxy.MethodWithError(); } catch(FaultException<ExceptionDetai1> exception) { Debug.Assert(exception.Detail.Type -- typeof(InvalidOperationException) .ToStringO); Debug.Assert(exception.Message == "Some error"); ) Декларативная передача исключений Атрибут ServiceBehavior содержит логическое свойство IncludeExceptionDetailsIn- Faults, которое определяется следующим образом: [Attr1buteUsage(AttrlbuteTargets.Class)] public sealed class ServiceBehaviorAttribute : Attribute. ... { public bool IncludeExceptionDetailInFaults {get;set:} //... } Значение IncludeExceptionDetailsInFaults по умолчанию равно false. Если за- дать ему true, как в следующем фрагменте: [ServiceBehaviorUncludeExceptionDetail InFaults = true)] class MyService : IMyContract {...}
Контракты сбоев 225 результат будет тем же, что и в листинге 6.4, но все произойдет автоматически: все неконтрактные сбои и исключения, инициированные службой или любы- ми нисходящими объектами, доставляются клиенту и включаются в возвращае- мое сообщение для обработки клиентской программой, как показано в листин- ге 6.5: [ServiceBehavior(IncludeExceptionDetailInFaults - true)] class MyService : IMyContract I public void MethodWithError() ( throw new InvalidOperationExceptionCSome error"); Сбои, инициированные службой (или ее нисходящими объектами) и при- сутствующие в контракте сбоев, доставляются клиенту в исходном виде. Передача всех исключений полезна для отладки, однако вы должны быть очень внимательны, чтобы не допустить распространения и развертывания служб с истинным свойством IncludeExceptionDetailsInFauLts. Для предотвращения по- тенциальных проблем можно воспользоваться условной компиляцией, как по- казано в листинге 6.6. Листинг 6.6. Свойство IncludeExceptionDetailsInFaults истинно только в отладочной версии public static class DebugHelper I public const bool IncludeExceptionDetailInFaults = lit DEBUG true; felse false: fendif [ServiceBehavior! IncludeExceptionDetail InFaults = DebugHelper.IncludeExcept i onDetai11nFaults)] class MyService ; IMyContract (...) ВНИМАНИЕ------------------------------------------------------------------------------------- При истинном свойстве IncludeExceptionDetailsInFaults исключение приведет к отказу канала, поэто- иу клиент не сможет выдавать последующие вызовы. Хост и диагностика исключений Очевидно, передача информации об исключениях в сообщениях о сбоях значи- тельно упрощает отладку, но она также находит применение при диагностике проблем в уже развернутых службах. К счастью, свойство IncludeExceptionDe- tailsInFaults может задаваться на уровне хоста — как программным, так и адми- нистративным способом в конфигурационном файле хоста. Если свойство зада-
226 Глава 6. Сбои и исключения ется на программном уровне, перед открытием хоста следует найти поведение в описании службы и установить свойство IncludeExceptionDetailsInFaults: ServiceHost host = new ServiceHost(typeof(MyService)): ServiceBehaviorAttribute debuggingBehavior = host.Descri pt1 on.Behaviors.F1nd<Servi ceBehavlorAttrlbute>(): debuggingBehavior.IncludeExceptionDeta11 InFaults « true; host.Open( ); Для удобства эту процедуру можно инкапсулировать в классе ServiceHost<T>, как показано в листинге 6.7. Листинг 6.7. ServiceHost<T> и возврат неизвестных исключений public class ServiceHost<T> : ServiceHost { public bool IncludeExceptionDeta11 InFaults { set { if(State == Communicationstate.Opened) { throw new InvalidOperationExceptionCHost is already opened"); } ServiceBehaviorAttribute debuggingBehavior = Description.Behaviors.F1nd<Serv1ceBehav1orAttr1bute>(); debuggingBehavior.IncludeExceptlonDetallInFaults = value: } get { ServiceBehaviorAttribute debuggingBehavior = DescriptIon.Behaviors.Flnd<ServiceBehavlorAttribute>(); return debuggingBehavior.IncludeExceptlonDetallInFaults: } } } Код использования ServiceHost<T> тривиален и компактен: ServiceHost<MyService> host = new ServiceHost<MyService>(): host.IncludeExceptlonDetal1 InFaults = true: host.OpenO: Чтобы применить это поведение на административном уровне, включите секцию нестандартного аспекта поведения в пользовательский файл конфигу- рации и включите ссылку на него в определение службы, как показано в лис- тинге 6.8. Листинг 6.8. Административная передача информации об исключениях в сообщениях о сбоях <system.serviceModel> <services> <service name = "MyService" behaviorConfiguration = "Debuggings </servlce> </services> <behaviors>
Контракты сбоев 227 <serviceBehaviors> <behavior name = "Debuggings <serviceDebug IncludeExceptionDetailInFaults = "true’7> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel> К преимуществам административной настройки в данном случае стоит отне- сти возможность смены поведения во время эксплуатации без изменения кода службы. Сбои и обратный вызов Конечно, обратные вызовы, обращенные к клиенту, могут завершиться не- удачей из-за коммуникационных исключений или из-за того, что сам обратный вызов инициировал исключение. Операции контрактов обратного вызова, как и операции контрактов служб, тоже могут определять контракты сбоев (лис- тинг 6.9). Листинг 6.9. Контракт обратного вызова с контрактом сбоев [ServiceContract (Ca 11 backContract = typeof(IMyContractCallback))] interface IMyContract I [Operationcontract] void DoSomethingf ); interface IMyContractCallback I [Operationcontract] [Faul tContract (typeofC I nval 1 dOperati onExcepti on)) ] void OnCal 1 Back( ); ПРИМЕЧАНИЕ---------------------------------------------------------------------------- Обратные вызовы в WCF обычно настраиваются как односторонние, поэтому они не могут опреде- лять собственных контрактов сбоев. Тем не менее, в отличие от нормальных вызовов служб, проявление ошибки также зависит от следующих факторов: О способа активизации обратного вызова; иначе говоря, был ли он направлен непосредственно при вызове службы вызывающему клиенту или это внепо- лосное обращение от другого объекта на стороне хоста; О используемой привязки; О типа инициированного исключения. При внеполосной активизации (то есть не от самой службы, а от другой сто- роны во время операции службы) обратный вызов ведет себя как обычный вы- зов операции WCF. В листинге 6.10 продемонстрирован внеполосный вызов для контракта обратного вызова, определенного в листинге 6.
228 Глава 6. Сбои и исключения Листинг 6.10. Обработка сбоев при внеполосных вызовах [ServiceBehaviordnstanceContextMode = InstanceContextMode.PerCai 1)] class MyService : IMyContract { static List<IMyContractCallback> m_Callbacks = new List<IMyContractCallback>( ); public void DoSomething( ) { IMyContractCallback callback = OperationContext.Current.GetCallbackChannel<IMyContractCallback>(): if(m_Callbacks.Contains(calIback) == false) { m_Ca11 back s.Add(callback); ) ) public static void CallClientst ) { Action<IMyContractCallback> invoke = delegate(IMyContractCallback callback) ( try { cal Iback.OnCallbackO; } catch(FaultException<InvalidOperationException> exception) catchtFaultException exception) {...} catch(Conwuni cat i onExcept i on except i on) }: m_Ca11 backs.ForEach(i nvoke): } } Если клиентский обратный вызов инициирует исключение, указанное в кон- тракте сбоев обратного вызова, или если обратный вызов инициирует Fault- Exception, это не приведет к отказу канала обратного вызова; вы можете пере- хватить исключение и продолжать пользоваться каналом. Впрочем, как и в слу- чае с обычными вызовами служб, после возникновения исключений, не входя- щих в контракт сбоев, пользоваться каналом обратного вызова не стоит. Если обратный вызов инициируется службой во время операции службы и исключение указано в контракте сбоев или если клиентский обратный вызов инициирует FaultException, сбой обратного вызова ведет себя так же, как при внеполосной активизации: [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)] class MyService : IMyContract { public void DoSomething( ) { IMyContractCallback callback = OperationContext.Current.GetCallbackChannel<IMyContractCallback>(); try
Контракты сбоев 229 cal Iback.OnCal1Back(); ) catch(Fau 1 tException<1 nt> exception) {...} ( 1 Обратите внимание: служба должна быть настроена в реентерабельном ре- жиме для предотвращения взаимной блокировки (см. главу 5). Поскольку опе- рация обратного вызова определяет контракт сбоев, она гарантированно не яв- ляется односторонним методом, отсюда и необходимость в реентерабельности. И внеполосные вызовы, и обратные вызовы службы ведут себя интуитивно понятным образом. Ситуация заметно усложняется, если служба активизирует обратный вызов, а операция обратного вызова инициирует исключение, не входящее в контракт сбоев (и не являющееся FaultException). Если служба использует привязку TCP или IPC, то при выдаче исключения, не входящего в контракт, клиент, обратившийся к службе с исходным вызовом, немедленно получает CommunicationException, даже если исключение было пере- хвачено службой. Затем служба получает FaultException. Служба может перехва- тить и обработать исключение, но исключение приводит к отказу канала и его дальнейшее использование службой невозможно: [ServiceBehavior (ConcurrencyMode = ConcurrencyMode.Reentrant)] class MyService : IMyContract ( public void DoSomething( ) ( IMyContractCalIback callback = Operationcontext.Current.GetCal1 backchannel<IMyContractCal1back>(): try ( callback.OnCallBackO; I catch(FaultException exception) (...) ) ) Если служба использует двойственную привязку WS, то при выдаче исклю- чения, не входящего в контракт, клиент, обратившийся к службе с исходным вызовом, немедленно получает CommunicationException, даже если исключение было перехвачено службой. При этом служба блокируется, а блокировка сни- мается со временем по исключению тайм-аута: [Servi ceBehavi or (ConcurrencyMode = ConcurrencyMode.Reentrant)] class MyService : IMyContract f public void DoSomething( ) ( IMyContractCalIback callback = Operati onContext.Current.GetCa11 backchannel<IMyContractCa11back>(): try
230 Глава 6. Сбои и исключения { ca 11 back.ОпСа11 Back( ); } catchCTimeoutException exception) {...} } } Повторное использование канала обратного вызова службой невозможно. ПРИМЕЧАНИЕ------------------------------------------------------------------------- Описанные различия в поведении при обратном вызове являются архитектурным недостатком WCF; иначе говоря, они были спроектированы, а не являются результатом ошибки. Возможно, проблема будет частично решена в будущих версиях. Отладка обратных вызовов Хотя для обратных вызовов может использоваться способ, представленный в листинге 6.4 (ручная передача исключения в сообщении о сбое), у атрибута CaLLbackBehavior имеется логическое свойство IncLudeExceptionDetaiLInFauLts; оно используется для передачи в сообщении всех исключений, не входящих в кон- тракт сбоев (кроме FaultException): [Attri buteUsage(AttгibuteTargets.Cl ass)] public sealed class CallbackBehaviorAttribute : Attribute.... { public bool IncludeExceptionDetailInFaults {get:set:} //... } Как уже упоминалось ранее, передача исключений является эффективным средством отладки: [Cal 1backBehavior(IncludeExceptionDetailInFaults = true)] class MyClient : IMyContractCalIback { public void OnCallBack( ) { throw new InvalidOperationException( ); } } Описанное поведение также можно задать на административном уровне в клиентском конфигурационном файле: <client> <endpoint ... behaviorConfiguration = "Debug" /> </client> <behaviors> <endpointBehaviors> <behavior name = "Debug"> <callbackDebug IncludeExceptionDetailInFaults = "true"/> </behavior>
Расширения обработки ошибок 231 </endpointBehaviors> </behaviors> Обратите внимание на использование тега endpointBehaviors для воздействия наконечную точку обратного вызова. Расширения обработки ошибок WCF позволяет разработчику изменить стандартную схему передачи исключе- ний и даже устанавливать свои перехватчики (hooks). Расширение применяет- ся на уровне канального диспетчера, хотя вам, по всей вероятности, потребует- ся просто применить его для всех диспетчеров. Чтобы установить собственное расширение обработки ошибок, необходимо предоставить диспетчерам реализацию интерфейса lErrorHandter, определяемого следующим образом: public interface lErrorHandler ( bool HandleError(Exception error): void ProvideFault(Exception error.Messageversion version, ref Message fault); ) Реализация может быть предоставлена любой стороной, но обычно она пре- доставляется либо самой службой, либо хостом. Более того, несколько расши- рений обработки ошибок можно объединить в цепочку. Позднее в этом разделе будет показано, как устанавливаются расширения. Метод ProvideFaultQ Метод ProvideFault() объекта расширения вызывается немедленно после того, как служба или любой объект в нисходящей цепочке вызовов инициирует лю- бое исключение. WCF вызывает ProvideFauLtQ перед тем, как возвращать управ- ление клиенту, перед завершением сеанса (если он имеется) и перед уничтоже- нием экземпляра службы (если требуется). Поскольку ProvideFault() вызывается во входном потоке вызова, пока клиент блокируется в ожидании завершения операции, следует избегать выполнения длительных операций в ProvideFault(). Использование ProvideFaultQ Метод ProvideFaultQ вызывается независимо от типа инициированного исклю- чения, будь то обычное исключение CLR, контрактный или неконтрактный сбой. Параметр error содержит ссылку на только что инициированное исключе- ние. Если ProvideFaultQ не делает ничего, то клиент получает исключение в со- ответствии с контрактом сбоев (если он присутствует) и типом исключения, как было описано ранее в этой главе: class MyErrorHandler : lErrorHandler public bool HandleError(Exception error) public void ProvideFault(Exception error.Messageversion version.
232 Глава 6. Сбои и исключения ref Message fault) { // Ничего не делаем - исключение проходит, как обычно } } Однако метод ProvideFault() может проанализировать параметр error и либо вернуть его клиенту в исходном виде, либо предоставить альтернативный сбой. Это относится даже к исключениям, входящим в контракт сбоев. Чтобы пре- доставить альтернативный сбой, необходимо воспользоваться методом Create- MessageFault() объекта FaultException для создания сообщения альтернативного сбоя. Если вы предоставляете новое сообщение для контракта сбоев, необходи- мо создать новый объект детализации и при этом нельзя использовать исход- ную ссылку error. Затем созданный объект передается статическому методу CreateMessage() класса Message: public abstract class Message { public static Message CreateMessage(MessageVersion version. MessageFault fault.string action): //... } Обратите внимание: при вызове CreateMessage() также необходимо передать информацию об используемом действии (action). Пример представлен в лис- тинге 6.11. Листинг 6.11. Создание альтернативного сбоя class MyErrorHandler : lErrorHandler { public bool HandleError(Exception error) {...} public void ProvideFault(Exception error.Messageversion version, ref Message fault) { FaultException<int> faultException = new FaultException<int>(3): MessageFault messageFault = faultException.CreateMessageFault(): fault = Message.CreateMessage(version. messageFault.faultExcepti on.Action); } } В листинге 6.11 метод ProvideFault() передает FaultException<int> значение 3 в качестве сбоя, инициированного службой, — независимо от того, какое ис- ключение было инициировано в действительности. Реализация ProvideFault() также может задать параметр fault равным null: class MyErrorHandler : lErrorHandler { public bool HandleError(Exception error) {•••} public void ProvideFault(Exception error.Messageversion version. ref Message fault)
Расширения обработки ошибок 233 fault = null: //Подавление любых сбоев в контракте 1 Это приведет к тому, что все исключения — даже соответствующие контрак- ту сбоев — будут доставляться клиенту в виде FaultException. Задание fault рав- ным null — эффективный способ подавления существующих контрактов сбоев. Повышение исключений Одно из возможных применений Provide Fa u lt() — методика, которую я называю повышением исключения. Служба может использовать в своей работе нисходя- щие объекты. Вызовы к таким объектам поступают от нескольких разных служб. В интересах логической изоляции эти объекты могут и не знать о конкретных контрактах сбоев служб, от которых поступает вызов. При возникновении оши- бок они просто инициируют обычные исключения CLR. Служба в такой ситуа- ции может проанализировать исключение при помощи расширения обработки ошибок. Если исключение относится к типу Т, a FaultException<T> входит в кон- тракт сбоев операции, служба может повысить исключение до полноценного FaultException<T>. Для примера возьмем следующий контракт службы: interface IMyContract I [Operationcontract] [Faul tCont ract (typeof( I nval 1 dOperat 1 onExcept i on)) ] void MyMethod( ); 1 Если нисходящий объект инициирует InvalidOperationException, ProvideFault() повысит его до FaultException<InvalidOperationExceptions как показано в листин- ге 6.12. Листинг 6.12. Повышение исключений class MyErrorHandler : lErrorHandler ( public bool HandleErrorCException error) {...} public void ProvideFault(Except!on error.Messageversion version. ref Message fault) ( if(error is InvalidOperationException) FaultException<InvalidOperationException> faultException - new FaultException<InvalidOperationException>( new InvalidOperationException(error.Message)): MessageFault messageFault = faultException.CreateMessageFaultO. fault = Message.CreateMessage(version.messageFault. faultException.Action); У кода в листинге 6.12 имеется один недостаток: код привязывается с кон- кретному контракту сбоев, а для его реализации во всех службах потребуется
234 Глава 6. Сбои и исключения большая, утомительная работа, не говоря уже о том, что любые изменения в контракте сбоев потребуют изменения расширения. К счастью, задачу повышения исключений можно автоматизировать при по- мощи моего статического класса ErrorHandLerHeLper: public static class ErrorHandlerHelper { public static void PromoteExceptIon(Type servIceType. Exception error. Messageversion version, ref Message fault): //... } При вызове ErrorHandlerHeLper.PromoteException() в параметре передается тип службы. Метод анализирует все интерфейсы и операции указанного типа служ- бы с использованием рефлексии и ищет в них контракты сбоев для конкретной операции. Информация о сбойной операции получается посредством разбора объекта error. PromoteException() повышает исключение CLR до контрактного сбоя, если тип исключения совпадает с одним из детализирующих типов, опре- деленных в контракте сбоев операции. При использовании класса ErrorHandlerHelper листинг 6.12 сокращается до одной-двух строк программного кода: class MyErrorHandler : lErrorHandler { public bool HandleError(Except1on error) {...} public void ProvldeFault(Exceptlon error.Messageversion version, ref Message fault) { Type servIceType = ...: ErrorHandlerHelper.PromoteExcept1 on(serviceType.error. version.ref fault): } } Реализация PromoteException() не имеет особого отношения к WCF, поэтому в этой главе она не рассматривается. При желании вы можете изучить ее в ис- ходном коде, прилагаемом к книге. В реализации используются многие нетри- виальные концепции программирования C# — такие, как шаблоны и рефлек- сия, разбор строк, анонимные методы и позднее связывание. Обработка сбоев Метод HandleError() интерфейса lErrorHandler определяется следующим образом: bool HandleError(Except1on error): Метод HandleError() вызывается WCF после возврата управления клиенту. Он предназначен исключительно для использования на стороне сервера, и ни- чего из того, что он делает, не затрагивает клиента. HandleError() вызывается в отдельном программном потоке — не в том, который использовался для обра- ботки запроса службы (и вызова ProvideFault()). Наличие отдельного потока,
Расширения обработки ошибок 235 работающего в фоновом режиме, позволяет выполнять продолжительные опе- рации (например, подключение к базе данных) без отрицательного влияния на работу клиента. Так как вы можете установить цепочку из нескольких расширений обработ- ки ошибок, WCF также позволяет управлять тем, какие из расширений в цепоч- ке должны использоваться в конкретном случае. Если HandLeError() возвращает false, то WCD продолжает вызывать HandLeError() для остальных установленных расширений. Если HandLeError() вернет true, WCF перестает вызывать расшире- ния. Разумеется, большинство расширений должно возвращать false. Параметр error метода HandleError() определяет исходное инициированное ис- ключение. Традиционно HandleError() используется для ведения протоколов и трас- сировки, как показано в листинге 6.13. Листинг 6.13. Регистрация ошибок class MyErrorHandler : lErrorHandler public bool HandleError(Exception error) try { LogbookServiceCllent proxy = new LogbookServiceCl ient(); proxy.Log(.. proxy.Close(): catch () finally return false: } ) public void ProvideFault(Exception error.Messageversion version, ref Message fault) Служба Logbook В архиве примеров, прилагаемом к книге, содержится автономная служба Log- bookService, предназначенная для регистрации ошибок. LogbookService регистри- рует ошибки в базе данных SQL Server, в контракт службы входят операции для чтения записей и очистки журнала. В исходный код также включена про- стейшая программа просмотра журнала. Кроме регистрации ошибок, Logbook- Service позволяет явно заносить информацию в журнал, независимо от исклю- чений. Архитектура службы показана на рис. 6.1. Чтобы автоматизировать регистрацию ошибок с применением LogbookService, воспользуйтесь методом LogError() моего статического класса ErrorHandLerHelper: public static class ErrorHandlerHelper ( public static void LogError(Exception error): II... )
236 Глава 6. Сбои и исключения Рис. 6.1. LogbookService и программа просмотра В параметре error передается регистрируемое исключение. LogError() инкап- сулирует вызов LogbookService. Например, вместо листинга 6.13 оказывается достаточно одной строки: class MyErrorHandler : lErrorHandler { public bool Handl eError(Except!on error) { ErrorHandlerHelper.LogError(error); return false; } public void ProvideFault(Except!on error.MessageVersion version, ref Message fault) {•••} } Кроме непосредственной информации об исключениях, LogError() извлекает из исключения и переменных окружения данные, которые дают полное описа- ние ошибки и связанной с ней информации. Конкретнее, LogError() сохраняет следующую информацию: О где произошло исключение (имена компьютера и хостового процесса); О программный модуль, в котором произошло исключение (имя сборки, имя файла и номер строки); О тип и член типа, в котором произошло исключение; О дата и время исключения; О имя исключения и сообщение.
Расширения обработки