Главная страница   /   8.1. Простейшие динамические классы (Метапрограммирование в .NET

Метапрограммирование в .NET

Метапрограммирование в .NET

Кевин Хазард

8.1. Простейшие динамические классы

Есть не так много открытых классов в пространстве имен System.Dynamic, но есть 13 классов в этом пространстве имен с термином Binder. Ближе к концу этой главы мы рассмотрим связывающие элементы и их решающую роль в том, что DLR называет динамической диспетчеризацией (dynamic dispatch). Из оставшихся пяти классов в пространстве имен только два использует средний разработчик: ExpandoObject и DynamicObject. Оба класса являются полезными для метапрограммирования и для написания более гибкого кода в целом.

Класс ExpandoObject

ExpandoObject имеет одно из самых смешно звучащих имен в .NET Framework. Для носителей английского языка название этого класса ассоциируется с эластичностью. Какую растяжимость подразумевает такое название? Может ExpandoObject быть коллекционным классом, который разрастается автоматически при вставке новых элементов в него? На самом деле, ExpandoObject является коллекцией. Если внимательно изучить документацию для класса, вы заметите, что ExpandoObject реализует интерфейс IDictionary<string, Object>. Код, подобный этому, должен был бы быть возможным:

ExpandoObject elastic = new ExpandoObject();
elastic["phrase"] = "Hello, world";

Но при компиляции этой крошечной части кода вы получите сообщение об ошибке, говорящее, что индексатор не доступен. Почему ExpandoObject не ведет себя как словарь, если он объявлен таковым? Потому что ExpandoObject реализовал интерфейс словаря явно. Таким образом, следующий модифицированный фрагмент кода будет скомпилирован:

IDictionary<string, object> flex = new ExpandoObject();
flex["phrase"] = "Hello, world!";

Как оказалось, для явно объявленного интерфейса необходимо объявить переменные, по которым вы намерены получить доступ к членам как определенным типам интерфейса. Эта особенность языка C# используется чаще всего не очень изобретательными интервьюерами, ищущими способ взволновать кандидатов. Иногда бывает и полезно скрыть реализацию интерфейса в определенной степени, как Microsoft сделала в данном случае. Причина для реализации интерфейса словаря вообще может стать очевидной после изучения этого кусочка кода:

IDictionary<string, object> flex = new ExpandoObject();
flex["phrase"] = "Hello, world!";
Console.WriteLine(((dynamic)flex).phrase);

Вы можете быть удивлены, что это будет компилироваться, успешно запустится и выведет выражение "Hello, World!" в окно консоли. Вы заметили, что в последней строке фрагмента кода не было кавычек, окружающих слово phrase? Оно используется как имя свойства. Как компилятор C# может разрешить доступ к свойству, которое не существует? Ответ заключается в преобразовании переменной flex как dynamic, и обработке переменных С#, помеченных таким образом.

Примечание

Динамические типы в C# немного походят на «согни ложку» из фильма Матрица. Когда мальчик объясняет Нео в знаменитой сцене: "Не пытайся согнуть ложку – это невозможно. Вместо этого попытайся понять истину: ложки нет". Точно так же нет никакого базового типа в C#, представляющего ключевое слово dynamic. Если разобрать немного скомпилированного C# кода, использующего динамические типы, вы увидите, что динамические элементы объявлены как System.Object. Это обработка C# компилятора тех простых объектов, что позволяет им вести себя динамически. Не пытайтесь понять dynamic как тип. Вместо этого попытайся понять истину: динамического типа нет.

К настоящему времени, мы надеемся, вы начинаете улавливать связь между реализацией ExpandoObject от IDictionary<string, Object> и возможностью доступа к элементам этой коллекции как к динамическим свойствам. Это возможно из-за ключевого слова С# dynamic и другого интерфейса DLR, называемого IDynamicMetaObjectProvider. Мы обсудим этот интерфейс и концепцию так называемых метаобъектов подробно далее в этой главе. В настоящее время мы обратимся к возможности, предоставляемой этим интерфейсом, динамическому связыванию. Это то, что добавляет динамическое ощущение в язык программирования C#, которым программисты Python, Ruby и JavaScript наслаждались на протяжении многих лет.

Кусок кода, показанный ранее, более громоздкий, чем он должен быть. В конце концов, люди в Microsoft облегчили получение специальных для этого случая свойства из ExpandoObject. Должно быть также максимально просто получить их в динамический объект. Хитрость заключается в том, чтобы объявить ExpandoObject как dynamic с самого начала:

dynamic flex = new ExpandoObject();
flex.phrase = "Hello, world!";
Console.WriteLine(flex.phrase);

Так гораздо проще, не правда ли? Этот новый фрагмент скомпилируется и запустится, чтобы вывести тот же результат, что и последний пример. Вам больше не придется обрамлять имя свойства phrase в кавычки или использовать индексатор словаря, чтобы установить в него строку "Hello, World!". Поскольку ExpandoObject объявлен как dynamic, он получает динамическую обработку от компилятора C# вместо статической обработки, делая эту работу для вас и используя знакомый синтаксис С# для доступа к свойствам.

Это также справедливо при передаче объектов в качестве параметров функции. Например, в листинге 8-1 показана функция, которая называется TestBag; она выполняет параметр с именем bag, добавляя данные и код в него динамически. Параметр bag объявлен с использованием ключевого слова dynamic, что дает ему специальную обработку C# компилятором в функции.

Листинг 8-1: Функция TestBag
static void TestBag(dynamic bag)
{
	bag.Listen =
		new Func<string>(() => Console.ReadLine());
	bag.Say =
		new Action<string>(s => Console.Write(s));
	bag.Say("What's your ID? ");
	bag.ID = bag.Listen();
	bag.Say("Hello, " + bag.ID + "." +
		Environment.NewLine);
}

Примечание

Параметр в функции TestBag из листинга 8-1 назван bag по определенной причине. Такие классы, как ExpandoObject, иногда называются «мешками свойств» (property bags), потому что вы можете «вбросить» в них значения, а позже достать их по имени. «Мешкам свойств» не нужны формальные декларации, чтобы содержать данные любых форм. Они обеспечивают специальный доступ к различным данным, поэтому они называются «мешками», чтобы укрепить метафору у вас в уме.

Обратите внимание в листинге 8-1 на то, что вы можете «вбрасывать» не только свойства в «мешок», но также и функции. Функции Listen и Say присваиваются параметру bag также легко, как и свойство ID. Метод Listen читает строку текста из консоли. Но вы можете легко изменить код во время выполнения путем присвоения другой функции, которая вызывает веб сервис, чтобы получить необходимые входные данные. Аналогично, функция Say, которая выводит на консоль, также может быть заменена другой, которая выводит строку в другое место. С такой гибкостью, возможно, вы начинаете понимать, как DLR может включить простые, но убедительные сценарии метапрограммирования в код.

Вызов функций, которые были добавлены динамически в метод TestBag, также вполне естественен, как вы можете видеть в листинге 8-1. Чтобы вызвать один из вновь добавленных методов, вы используете оператор доступа к элементу (точку) для объекта dynamic, имя вызываемой функции и передаете необходимые параметры в круглых скобках. Это стандартный C# синтаксис для любой операции вызова функции. Поскольку параметр bag был объявлен как dynamic, C# включает весь необходимый код для доступа к возможности динамического связывания ExpandoObject, чтобы вызвать функции членов по имени.

Примечание

Среди так называемых принципов объектно-ориентированного программирования SOLID "L" обозначает принцип подстановки Барбары Лисков (LSP). Простая идея LSP заключается в том, что замена объекта экземпляром одного из его подтипов не должно сломать программу. В основе LSP лежит идея, что типы реализуют контракты. Например, если потребляющий код ожидает того, чтобы функция с именем Listen существовала в объекте, она должен быть там или программа сломается. Но если функция может быть внедрена динамически при помощи ExpandoObject, традиционные подтипы нарушают или удовлетворяют LSP?

Класс DynamicObject

ExpandoObject является весьма полезным, но он помечен как sealed в библиотеке классов .NET Framework. Вы не можете использовать его в качестве базового класса, чтобы разрешить динамическое связывание для других типов данных. Вы можете использовать ExpandoObject в качестве модели, реализуя довольно сложный интерфейс IDynamicMetaObjectProvider, чтобы создать аналогичное динамическое поведение, но это требует достаточно глубокого понимания деревьев выражений и других сложных концепций.

К счастью, DLR предоставляет еще один класс в пространстве имен System.Dynamic, который не является sealed и обеспечивает хороший набор методов для избирательной реализации динамического связывания для ваших собственных классов. Базовый класс DynamicObject обеспечивает следующие 12 открытых, виртуальных методов, которые могут быть избирательно переопределены, чтобы включить конкретные виды поведения при работе с DLR-совместимым языком:

  • TryBinaryOperation: включает бинарные операторы, как сложение (+) или вычитание (-), и так далее.
  • TryConvert: разрешает преобразование к статически известным типам.
  • TryCreateInstance: позволяет создание экземпляров базовых типов данных, которые могут быть необходимы для поддержки динамического объекта.
  • TryDeleteIndex: разрешает удаление индексированного элемента коллекции (не поддерживается синтаксисом C# или Visual Basic).
  • TryDeleteMember: разрешает удаление свойства или функции члена (не поддерживается синтаксисом C# или Visual Basic).
  • TryGetIndex: разрешает получение значения индексированного элемента коллекции.
  • TryGetMember: разрешает получение значения свойства.
  • TryInvoke: включает вызов динамического объекта как функции.
  • TryInvokeMember: включает вызов членов как функции.
  • TrySetIndex: позволяет мутацию индексированного элемента коллекции.
  • TrySetMember: позволяет мутацию свойства элемента или назначение реализации функции элемента.
  • TryUnaryOperation: включает унарные операторы, как инкремент (++) и декремент (--).

Эти два слова повторяются в течение все списка: пробовать и позволяет (try и enables). Эти слова важны, поскольку они определяют дух класса DynamicObject. При использовании класса DynamicObject в качестве базового класса вы включаете различные функции динамического связывания путем переопределения этих виртуальных методов по мере необходимости. Так называемый метаобъект в DynamicObject вызовет переопределенные методы, когда диспетчеризатор связывания во время выполнения вызывается в нем.

Если, например, вы не хотите, чтобы динамический тип обрабатывался как массив (при помощи индексного оператора С# ([])), вам не нужно переопределять методы TryGetIndex и TrySetIndex в классе. Но если через некоторое время потребитель вашего класса попытается использовать оператор индекса при доступе к динамическому объекту, базовый класс выбросит исключение во время выполнения, потому что он не сможет найти реализацию для запрошенной операции.

Чтобы реализовать это, давайте создадим свою собственную версию ExpandoObject, метко названную ElastoObject, как показано в следующем листинге.

Листинг 8-2: Исходный код ElastoObject
class ElastoObject : DynamicObject
{
	Dictionary<string, object> members =
		new Dictionary<string, object>();
	public override bool TrySetMember(
		SetMemberBinder binder, object value)
	{
		if (value != null)
			members[binder.Name] = value;
		else if (members.ContainsKey(binder.Name))
			members.Remove(binder.Name);
		return true;
	}
	public override bool TryGetMember(
		GetMemberBinder binder, out object result)
	{
		if (members.ContainsKey(binder.Name))
		{
			result = members[binder.Name];
			return true;
		}
		return base.TryGetMember(binder, out result);
	}
	public override bool TryInvokeMember(
		InvokeMemberBinder binder, object[] args,
		out object result)
	{
		if (members.ContainsKey(binder.Name))
		{
			Delegate d = members[binder.Name] as Delegate;
			if (d != null)
			{
				result = d.DynamicInvoke(args);
				return true;
			}
		}
		return base.TryInvokeMember(binder, args, out result);
	}
}

ElastoObject, показанный в листинге 8-2, ведет себя почти так же, как DLR ExpandoObject. На самом деле, с помощью функции, показанной в листинге 8-1, следующие две строки кода будут вести себя одинаково:

Ключ к возможности динамического связывания ElastoObject начинается с наследования от базового класса DLR DynamicObject. Внутренне, ElastoObject создает Dictionary<string, object> для хранения пар имя-значение, но он не реализует интерфейс, специфичный для этой возможности, как это делает ExpandoObject. Мы будем использовать это различие, чтобы показать, как можно раскрыть подобную словарю функциональность без интерфейса, в следующем разделе. Для обработки вызова из рантайм биндера и метаобъекта DynamicObject, есть три переопределенных варианта в ElastoObject:

  • TrySetMember
  • TryGetMember
  • TryInvokeMember

Остальные девять виртуальных функций в базовом классе DynamicObject не переопределены в ElastoObject, потому что вам не нужны эти виды динамического связывания, чтобы имитировать поведение ExpandoObject. Каждый из трех реализованных переопределенных вариантов принимает конкретный для операции связывающий класс в качестве первого параметра. Метод TrySetMember принимает параметр типа SetMemberBinder, TryGetMember принимает параметр типа GetMemberBinder и так далее. Двенадцать из этих типов связывания определены в пространстве имен System.Dynamic, по одному для каждой из двенадцати операций связывания, поддерживаемых DLR-совместимыми языками. Каждый связующий класс может иметь свойства и методы для конкретного типа операции связывания, которая должна быть выполнена.

Переопределенный вариант TrySetMember обрабатывает установку новых свойств и методов в динамический объект. Имя свойства или метода для установки передается в свойство SetMemberBinder Name. Устанавливаемое значение передается как отдельный параметр и присваивается классу внутреннего словаря по имени для будущего использования. Возвращение true из TrySetMember сигнализирует метаобъекту, вызвавшему его, что элемент был успешно установлен.

Хотя язык С# синтаксически не поддерживают концепцию удаления членов класса во время выполнения, функция TrySetMember в ElastoObject предоставляет возможность сделать это. Рассмотрим следующую строку кода:

bag.Say = null;

Если вы добавили эту строку в конце функции, показанной в листинге 8-1, вы бы эффективно удалили функцию Say из класса во время выполнения. Метод TrySetMember считает null охранным значением, которое сигнализирует удаление членов из внутреннего словаря – даже функций, которые были добавлены в динамический класс. Любая попытка вызвать функцию Say после ее удаления может привести к ошибке выполнения.

DLR биндеры как "язык языков"

Если вы захотите определить дискретные операции, необходимые для того, чтобы сделать так, что любые два языка программирования сообщаются друг с другом, велики шансы, что вы в конечном итоге найдете что-то, напоминающее 12 методов связывания, определенных в DLR классе DynamicMetaObject.

12 виртуальных методов в классе DynamicObject, начинающиеся с Try, приятно отражают эти операции связывания. С их помощью вы можете создавать новые динамические объекты, конвертировать их, присваивать значения свойствам и новым функциям, выбирать значения свойств и вызывать функции. Вы даже можете использовать их для обработки ваших динамических объектов в виде массивов или выполнять унарные и бинарные операции с ними, используя синтаксис вашего любимого DLR-совместимого языка.

DLR обеспечивает надежный «язык языков», который можно считать если не инструментом метапрограммирования, то обобщенным фреймворком межпроцессного взаимодействия (Inter-Process Communication, IPC).

Переопределенный вариант TryGetMember обрабатывает выборку значений свойств. Свойство GetMemberBinder Name используется, чтобы найти нужную запись в словаре и вернуть ее вызывающему элементу через выходной параметр result. Если названный элемент не найден в словаре, реализация базового класса TryGetMember может быть запущена, что выбросит значимое, определенное для DLR исключение, что названный элемент не может быть найден. Опять же, возвращение true из этого метода сигнализирует метаобъекту, что выборка элемента прошла успешно.

Наконец, есть TryInvokeMember, который обрабатывает вызов функций для динамического класса ElastoObject. Как и TryGetMember, этот метод пытается найти названный элемент в связующем параметре. Но вместо того чтобы вернуть то, что он находит, метод TryInvokeMember преобразует это в Delegate и вызывает его функцию DynamicInvoke, передавая все параметры, предоставленные вызывающим элементом.

Одно из ключевых различий между ExpandoObject DLR и ElastoObject состоит в экспозиции реализации словаря, используемой для управления свойствами и методами элементов. ExpandoObject явно реализует интерфейс IDictionary<string, Object>, тогда как ElastoObject скрывает внутреннее использование дженерик класса Dictionary<string, object>. Вы можете спросить себя, поскольку если и ExpandoObject, и ElastoObject предназначены для динамического использования, почему ExpandoObject вообще раскрывает свой словарь через реализацию интерфейса? Почему бы вместо этого не использовать динамические методы диспетчеризации, как показано в следующем листинге?

Листинг 8-3: Добавление индексации в ElastoObject
public override bool TryGetIndex(
	GetIndexBinder binder, object[] indexes,
	out object result)
{
	string name = indexes[0] as string;
	if (members.ContainsKey(name))
	{
		result = members[name];
		return true;
	}
	return base.TryGetIndex(binder, indexes, out result);
}

Если бы TryGetIndex, приведенный в листинге 8-3, был добавлен к ElastoObject из листинга 8-2, была бы добавлена новая интересная динамическая возможность. В частности, вы могли бы написать код, подобный этому, используя листинги, представленные в этой главе:

dynamic squishy = new ElastoObject();
TestBag(squishy);
Delegate shout = squishy["Say"];
string id = squishy["ID"];
shout.DynamicInvoke("Howdy, " + id + ".");

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

Если разобрать DLR ExpandoObject, вы увидите, что он обеспечивает гораздо более надежную реализацию «мешка» свойства и функции, чем ElastoObject, показанный здесь. Мы рекомендуем вам самим разобрать и посмотреть. Анализирование кода в библиотеке классов .NET Framework от Microsoft является отличным способом учиться. Но даже и без этого, вы должны признать по простому классу ElastoObject, показанному здесь, что написание собственных динамических типов с использованием DLR DynamicObject в качестве базового класса – это не сложно.

Теперь, когда мы успешно имитировали ExpandoObject, давайте взглянем на реальный пример динамического типа в действии.

Динамический парсинг Open Data Protocol

В 2011 году IDC (спонсированный EMC) подсчитал, что было создано 1,2 зеттабайт данных. В следующем году оценка была 1,8 зеттабайт. Это почти два триллиона гигабайт информации. Тот, кто работает в богатой данными бизнес среде в наше время, понимает, что многие из этих данных неструктурированные или слабоструктурированные по своей природе. Это исследование также показало, что рост объема данных в ближайшее десятилетие будет превышать 7500 процентов, в то время как рост IT персонала вырастет на сравнительно скромные 150 процентов.

Один из способов решения этой проблемы лежит в расширяемых системах, как Open Data Protocol (OData). Отличная глубина выражения в OData основана на Common Schema Definition Language (CSDL) и его Entity Data Model (EDM). OData формат Atom позволяет, чтобы богатые метаданными, очень расширяемые свойства были установлены и раскрыты для практически любого типа схемы. Одним из самых популярных интернет сервисов, поддерживающих OData, является Netflix. Чтобы запросить Atom (XML) документ для фильма Терминатор, может быть использован следующий запрос:

http://odata.netflix.com/Catalog/Titles
	?$filter=Name eq 'The Terminator'

Введите веб адрес в адресную строку браузера, чтобы увидеть Atom документ, описывающий фильм. Часть XML в следующем листинге показывает интересный фрагмент документа, который может быть возвращен.

Листинг 8-4: Фрагмент Netflix OData, описывающий фильм
<m:properties>
	<d:Name>The Terminator</d:Name>
	<d:Synopsis>In the post-apocalyptic ...</d:Synopsis>
	<d:AverageRating m:type="Edm.Double">3.9</d:AverageRating>
	<d:ReleaseYear m:type="Edm.Int32">1984</d:ReleaseYear>
	<d:Runtime m:type="Edm.Int32">6420</d:Runtime>
	<d:Rating>R</d:Rating>
	<d:Dvd>
		<d:Available m:type="Edm.Boolean">true</d:Available>
	</d:Dvd>
	<d:BluRay>
		<d:Available m:type="Edm.Boolean">true</d:Available>
	</d:BluRay>
	<d:Instant>
		<d:Available m:type="Edm.Boolean">true</d:Available>
	</d:Instant>
	<d:BoxArt>
		<d:SmallUrl>http://cdn-1.nflximg.com/...</d:SmallUrl>
	</d:BoxArt>
</m:properties>

В документах Atom, как показанном в листинге 8-4, префикс "m:" обозначает пространство имен метаданных Atom, тогда как "d:" означает свойство данных Atom. Обратите внимание, что некоторые из свойств в Netflix помечаются атрибутом типа данных. Например, свойство AverageRating объявлено как тип Edm.Double. Это хорошо известный, простой тип данных из CSDL EDM для чисел с плавающей точкой с точностью два знака. Кроме того, в фрагменте документа Netflix, обратите внимание на использование простых типов данных Edm.Int32 и Edm.Boolean. Это значимые метаданные, которыми в полной мере может воспользоваться любой код, предназначенный для разбора OData.

Теперь посмотрим на другой пример. Следующие URL получит материалы OData с ebay.com, для элементов, относящихся к Терминатору. Поскольку ebay.com может продавать товары любого типа, такой поисковый запрос может вернуть много пунктов:

http://ebayodata.cloudapp.net/Items?search=The Terminator

Следующий листинг показывает подмножество свойств, возвращаемых сервисом ebay.com OData для такого поиска.

Листинг 8-5: Фрагмент ebay.com OData для поиска
<m:properties>
	<d:Title>Terminator 3: Rise of the Machines (DVD)</d:Title>
	<d:TimeLeft>P0DT0H9M42S</d:TimeLeft>
	<d:Currency>USD</d:Currency>
	<d:CurrentPrice m:type="Edm.Double">
		2.5</d:CurrentPrice>
	<d:Country>US</d:Country>
	<d:GalleryUrl>http://thumbs...</d:GalleryUrl>
	<d:Condition>
		<d:Name>Like New</d:Name>
	</d:Condition>
	<d:ListingInfo>
		<d:ListingType>Auction</d:ListingType>
	</d:ListingInfo>
	<d:ShippingInformation>
		<d:ShippingServiceCost m:type="Edm.Double">
			3</d:ShippingServiceCost>
	</d:ShippingInformation>
</m:properties>

Говорить, что схемы Netflix OData и ebay.com OData отличаются – было бы не совсем правильным. В конце концов, они обе придерживаются той же CSDL спецификации, и они обе Atom-совместимы. Но если посмотреть на них, они действительно по-разному структурированы при помощи расширяемых свойств данных, доступных через Atom. Код для разбора этих двух разных способов подачи материала, безусловно, должна быть специализированным, в частности, потому, что один из них будет возвращать не более одного пункта, а другой может вернуть много. Динамический тип данных может помочь в решении этой проблемы в общем, раскрывая естественный, интегрированный с языком синтаксис для разбора любых OData.

Создание фреймворка класса DynamicOData

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

public delegate void DataReady(dynamic obj);

Теперь давайте определим пару пространств имен XML, обычно используемых в публикации материалов OData через Atom. Они предназначены для коллекции свойств метаданных и свойств данных, содержащихся в них, как показано в листингах 8-4 и 8-5. Событие типа делегата DataReady также будут выделено из класса:

public class DynamicOData
{
	public event DataReady OnDataReady;
	private const string odataNamespace =
		"http://schemas.microsoft.com/ado/" +
		"2007/08/dataservices";
	private const string metadataNamespace =
		odataNamespace + "/metadata";
}

Теперь давайте добавим классу средство для хранения ссылки на XML узел в пределах материала:

private IEnumerable<XElement> _current = null;

Для завершения базовой настройки вам нужно несколько конструкторов для класса. Конструктор по умолчанию, который не устанавливает _current XML элемент, пригодится. Два других конструктора помогут обработать два конкретных случая в XML при перемещении по иерархии документа: первый, когда один XML узел должен быть обернут как новый объект DynamicOData, а второй, когда последовательность узлов должна быть представлена таким образом:

public DynamicOData() { }
protected DynamicOData(XElement current)
{
	_current = new List<XElement> { current };
}
protected DynamicOData(
	IEnumerable<XElement> current)
{
	_current = new List<XElement>(current);
}

Асинхронная выборка и разбор материала OData

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

public void FetchAsync(string queryUrl)
{
	WebClient client = new WebClient();
	client.DownloadStringCompleted += OnDownloadCompleted;
	client.DownloadStringAsync(new Uri(queryUrl));
}

Этот класс должен также включать в себя метод, который будет вызываться, если возникнет событие WebClient DownloadStringCompleted:

private void OnDownloadCompleted(object sender,
	DownloadStringCompletedEventArgs e)
{
	string xml = (e != null || e.Error == null)
		? e.Result : String.Empty;
	if (xml != null)
	{
		var document = XDocument.Parse(xml);
		XNamespace ns = metadataNamespace;
		_current = document.Descendants(ns + "properties");
	}
	if (OnDataReady != null)
		OnDataReady(this);
}

Метод OnDownloadCompleted разбирает XML строку от сервера OData и присваивает узел-потомок, соответствующий пространству имен свойств метаданных Atom перечислению _current. Наконец, событие OnDataReady происходит для любого подписчика, чтобы сообщить о том, что данные готовы.

Добавление в класс динамического функционала TryGetMember

До сих пор в классе DynamicOData не было никакой динамической возможности. Он даже не наследуется от DLR класса DynamicObject в своем нынешнем виде. Давайте добавим в класс объявление:

public class DynamicOData : DynamicObject

Чтобы сделать свойства данных OData Atom доступными в DLR-совместимых языках как свойства для динамического класса, переопределите метод TryGetMember, как показано в следующем листинге. Этот метод раскрывает псевдосвойство Value, которое может быть использовано для получения значения текста в данном XML узле. Код работает так, что извлекает свойство Value первого XElement для коллекции _current.

Листинг 8-6: Добавление TryGetMember в класс DynamicOData
public override bool TryGetMember(
	GetMemberBinder binder, out object result)
{
	result = null;
	if (binder.Name == "Value")
	{
		XElement element = _current.ElementAt(0);
		result = _current.ElementAt(0).Value;
	}
	else
	{
		var items = _current.Descendants(
			XName.Get(binder.Name, odataNamespace));
		if (items == null || items.Count() == 0)
			return false;
		result = new DynamicOData(items);
	}
	return true;
}

Если названное свойство в параметре GetMemberBinder не является специальным псевдосвойством, код соберет XML потомков узла _current, упакует их в качестве нового объекта DynamicOData, используя один из конструкторов, описанных ранее, и вернет его. Возвращая новый объект DynamicOData для вновь открытых узлов, можно сцепить динамические доступы один за другим, чтобы пересечь XML иерархию.

Взгляните на фрагмент Netflix OData в листинге 8-4, где показано, как кодируется наличие DVD. Когда имеется простая функция TryGetMember, вы можете написать простой код, как этот, чтобы получить доступ к такому элементу:

DynamicOData movie = new DynamicOData();
movie.OnDataReady += title => {
	Console.WriteLine(title.Dvd.Available.Value); };
movie.FetchAsync(
	"http://odata.netflix.com/Catalog/Titles" +
	"?$filter=Name eq 'The Terminator'");

Это выведет строку "true" в окне консоли, если запрашиваемый фильм доступен на Netflix. Обратите внимание, как лямбда-выражение, присвоенное событию OnDataReady, сцепляет XML элементы в материале. Из-за способа, которым был объявлен делегат, параметр title динамически обрабатывается компилятором. Таким образом, вызов title.Dvd вызывает метод TryGetMember, который оборачивает XML узлы как новый объект DynamicOData и возвращает его. Отсюда продолжается динамическая обработка, поэтому узел Available аналогично доступен через TryGetMember и обернут как еще один новый объект DynamicOData. Наконец, запрашивается специальное псевдосвойство Value, поэтому реализация TryGetMember получает Value узла доступности DVD и возвращает его вызывающему элементу. Это вообще не похоже на XML парсинг, не так ли? Вместо этого, кажется, как будто мы имеем доступ к известным свойствам внутри OData, используя старый добрый C# синтаксис.

Одно из усовершенствований, которые бы хорошо сделать сейчас, – это воспользоваться данными типа CSDL EDM, встроенными в материал Atom. Посмотрите в листинге 8-4, что материал Netflix включает информацию type о наличии DVD. В частности, атрибут type включается так:

<d:Dvd>
	<d:Available m:type="Edm.Boolean">true</d:Available>
</d:Dvd>

Вместо того чтобы вернуть строку значения XML узла, почему бы не заставить динамический тип принудить значение к его объявленному типу и вернуть его вызывающему элементу? Вы можете сделать это, добавив немного парсингового кода в метод TryGetMember, где он обрабатывает псевдосвойство Value, как показано в следующем листинге.

Листинг 8-7: Принуждение к различным типам данных OData CSDL EDM
XAttribute typeAttr = element.Attribute(
	XName.Get("type", metadataNamespace));
if (typeAttr != null)
{
	string type = typeAttr.Value;
	if (type != null)
	{
		switch (type)
		{
			default:
				break;
				case "Null":
					result = null;
					break;
				case "Edm.Boolean":
					result = Convert.ToBoolean(result);
					break;
				case "Edm.Byte":
					result = Convert.ToByte(result);
					break;
				case "Edm.DateTime":
					result = Convert.ToDateTime(result);
					break;
				case "Edm.Decimal":
					result = Convert.ToDecimal(result);
					break;
				case "Edm.Double":
					result = Convert.ToDouble(result);
					break;
				case "Edm.Single":
					result = Convert.ToSingle(result);
					break;
				case "Edm.Guid":
					result = Guid.ParseExact(
					(string)result, "D");
					break;
				case "Edm.Int16":
					result = Convert.ToInt16(result);
					break;
				case "Edm.Int32":
					result = Convert.ToInt32(result);
					break;
				case "Edm.Int64":
					result = Convert.ToInt64(result);
					break;
				case "Edm.SByte":
					result = Convert.ToSByte(result);
					break;
				case "Edm.DateTimeOffset":
					result = DateTimeOffset.Parse(
					(string)result);
					break;
		}
	}
}

С кодом из листинга 8-7 внутри обработчика псевдосвойства TryGetMember Value, попробуйте запустить это небольшое упражнение, чтобы получить информацию о наличии DVD для фильма. Вы заметите, что теперь в окне консоли появляются слова "True" или "False", а не "true" или "false", соответственно. Визуально это небольшая разница, но на самом деле изменение является значительным. Когда слова "true" или "false" отображались на консоли, это произошло потому, что выводилась символьная строка в XML. Но после добавления кода для принуждения значения свойства к типу данных .NET, в зависимости от типа данных EDM, объявленных в основном XML, выходные "True" и "False" показывают, что вы работаете с возвращенными значениями из метода ToString для объекта System.Boolean.

Добавление динамического функционала TrySetMember в класс

Чтение XML из OData при помощи свободного синтаксиса C# – это здорово, но вы также можете изменить документ. Это очень просто: переопределите метод TrySetMember в классе DynamicOData, как показано в следующем листинге.

Листинг 8-8: Добавление TrySetMember в класс DynamicOData
public override bool TrySetMember(
	SetMemberBinder binder, object value)
{
	if (binder.Name == "Value")
	{
		_current.ElementAt(0).Value = value.ToString();
		return true;
	}
	return false;
}

Обратите внимание в листинге 8-8, что можно присвоить только псевдосвойство Value. Мы остановились тут для простоты, но можно, конечно, добавить больше функциональных возможностей методу TrySetMember, если для этого существуют важные причины. С помощью этого простого, нового метода вы можете теперь добавить следующие строки кода в лямбда-выражение, показанное ранее, для изменения фильма, полученного от сервиса Netflix:

title.Dvd.Available.Value = false;
title.AverageRating.Value = 4.1;

Это позволит установить возможность использования DVD и средний рейтинг в базовом XML документе. Запросы этих значений позже докажут, что изменения были записаны. Не то, чтобы Netflix позволил нам изменить эти значения, обратно отправив их сервису OData, но если бы он так сделал, измененный документ был бы хорошей отправной точкой.

Добавление динамического функционала TryGetIndex в класс

Не все запросы OData создадут один объект, как делают запросы Netflix. На самом деле, поскольку отдельные свойства в узле сами могут быть коллекциями других свойств и могут быть обернуты как новые объекты DynamicOData, наличие класса, который ведет себя как массив, может иногда иметь важное значение. Чтобы реализовать это, добавьте следующий метод:

public override bool TryGetIndex(
	GetIndexBinder binder, object[] indexes,
	out object result)
{
	int ndx = (int)indexes[0];
	result = new DynamicOData(
		_current.ElementAt(ndx));
	return true;
}

Для простоты будет принят только один числовой индекс. Вы можете добавить столько измерений индекса, сколько вы хотите, и они могут быть любого типа данных. Обратите внимание, что вы используете другой пользовательский конструктор, описанный в начале этого примера, оборачивая один XElement по указанному индексу как новый объект DynamicOData. Это позволяет использовать возвращенный объект для продолжения сцепливания решения:

DynamicOData ebayItem = new DynamicOData();
	ebayItem.OnDataReady += item => {
		Console.WriteLine(
			item[3].ShippingInformation
				.ShippingServiceCost.Value); };
ebayItem.FetchAsync(
	"http://ebayodata.cloudapp.net/ +
	"Items?search=The Terminator ");

Этот небольшой кусок кода отобразит стоимость доставки четырех товарных позиций в окне консоли. Обратите внимание на сходство между кодом, используемым для запроса Netflix, и этим кодом для запроса ebay.com. Класс DynamicOData позволяет обрабатывать все материалы OData аналогичным способом, значительно улучшая понимание программиста за счет уменьшения сложности кода. Если вы помните, это было одной из основных целей для применения методик метапрограммирования, о чем мы и заявили в начале книги.