Главная страница   /   8.2. Хостинговая модель DLR (Метапрограммирование в .NET

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

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

Кевин Хазард

8.2. Хостинговая модель DLR

Наверное, вы помните, как мы говорили в начале книги, что мета может означать после или рядом. Многие виды метапрограммирования, которые мы рассмотрели до сих пор, относятся к типу «после», то есть они меняют типы и классы после того, как те были созданы, чтобы изменить их поведение. Другие виды метапрограммирования – это виды «рядом», они создают типы на лету, чтобы удовлетворить ваши потребности во время выполнения. Скриптинг прекрасно вписывается в стиль метапрограммирования «рядом», потому что включает запуск кода вместе с другой системой, часто выступая как контроллер.

Есть много причин хотеть интегрировать основной язык приложения, скажем C#, со скриптовым языком, таким как Python или Ruby. Давайте рассмотрим один из наиболее интересных примеров этого типа и взглянем на псевдокод в следующем листинге. Не волнуйтесь о синтаксисе и тому подобном на данный момент. Прочитайте листинг и представьте себе, что бизнес пользователи приложения электронной коммерции написали этот псевдокод, чтобы выразить основные правила, которые регулируют дисконтирование продуктов, основываясь на их продвижении.

Листинг 8-9: Скрипт для дисконтирования товаров по типу в покупательской корзине
totalItems = 0
clothingItems = 0
for line in cart.LineItems:
	line.Discount = 0.0
	totalItems = totalItems + line.Quantity
	if line.Product.Category == 'Clothing':
		clothingItems = clothingItems + line.Quantity
clothingDiscount = 0.0
if clothingItems > 5:
	clothingDiscount = 0.09
elif clothingItems >= 2:
	clothingDiscount = 0.05
for line in cart.LineItems:
	if line.Product.Category == 'Clothing':
		line.Discount = clothingDiscount
	if totalItems >= 7:
		line.Discount = line.Discount + 0.03

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

  • Установить несколько переменных для подсчета товаров.
  • Пройти циклом по LineItems, содержащимся в чем-то, именуемом cart.
    Установить Discount для каждого LineItem на ноль.
    Накопить Quantity для каждого LineItem в totalItems.
    Для каждого LineItem, который имеет 'Clothing' Category для своего Product накопить Quantity для каждого LineItem в clothingItems.
  • Указать процент скидки для всех предметов одежды на основе количества, подсчитанного внутри цикла. По крайней мере, два clothingItems в cart имеют 5-процентную скидку, в то время как 5 или больше предметов имеют 9-процентную скидку.
  • Снова пройти циклом по LineItems в cart, устанавливая подсчитанный процент скидки для товаров в 'Clothing' Product Category. Чтобы еще привлечь покупателей, применяется дополнительная 3-процентная скидка к каждому элементу в корзине, если было приобретено 7 или более товаров.

Единственная концепция, которая не совсем понятна в этом, заключается в определении переменной cart и коллекции LineItem, которую она содержит. Где была определена cart? Скоро мы покажем вам, как внедрить старый добрый .NET объект (POCO), как cart, в скрипт, как этот, используя DLR хостинг.

Если к настоящему времени вы чувствуете себя комфортно с псевдокодом из листинга 8-9, пришло время раскрыть маленький секрет. Это вообще не псевдокод. Это рабочая Python программа, которую мы будем использовать в следующем разделе, чтобы продемонстрировать, как можно интегрировать скрипты в ваши приложения с помощью нескольких десятков строк кода. Однако также важным является понимание ключевых классов, которые существуют для DLR хостинга. Давайте познакомиться с рантайм движками и областями видимости: тремя из наиболее важных типовых групп для эффективного использования DLR хостинга.

Примечание

Дино Вилэнд из Microsoft, один из создателей DLR и реализации языка IronPython, описывает DLR как наличие двух слоев. Внутренний слой содержит классы .NET Framework, как DynamicObject, и различные биндеры, рассмотренные в предыдущем разделе. Эти классы являются частью ядра .NET, так что у каждого, кто устанавливает .NET Framework, они есть. Внешний слой DLR, в частности, API хостинга, существует в сборках, которые не поставляются с .NET Framework. Скачайте последнюю стабильную версию на http://ironpython.codeplex.com, чтобы выполнить упражнения в оставшейся части этого раздела.

Рантаймы, движки и области видимости

Когда вы начинаете работу с DLR хостингом, сам объем предметно-ориентированных классов может усложнить понимание концепций. Вы увидите такие классы в пространстве имен Microsoft.Scripting, как SourceUnit, ScopeVariable и ScriptCode. В пространстве имен Microsoft.Scripting.Hosting вы столкнетесь с классами, как CompiledCode, ScriptHost и ScriptSource. Все эти классы существуют в одной сборке с именем Microsoft.Scripting.dll, на которую вам нужно ссылаться в любом приложении, использующем DLR-совместимые скриптовые языки. Вы можете скачать эту сборку и ее исходный код на http://dlr.codeplex.com.

С наличием такого множества хостинговых классов, имеющих слово script в своих именах, сложно понять, с чего начать. Для простых скриптовых хостинговых сценариев существует три основных типа в DLR, которые вы должны хорошо понимать: ScriptRuntime, ScriptScope и ScriptEngine.

Класс ScriptRuntime

Как вы видите на рисунке 8-1, ScriptRuntime - это нечто вроде мастер-объекта в хостинговой модели DLR.

Рисунок 8-1: Основными классами в хостинговом API DLR являются ScriptRuntime, ScriptScope and ScriptEngine. Чтобы использовать хостинговый API, всегда начинайте с создания ScriptRuntime, указывая скриптовые языки, которые вы хотите сделать доступными.

Тут представлено глобальное состояние скрипта для ScriptEngine и связанной с ним областью видимости, а также объектами исполняемого кода. Любое приложение, использующее DLR-совместимый скриптовый язык, начнет с использования конструктора для рантайм класса или с вызова его метода статической фабрики CreateFromConfiguration для загрузки данных по установке языка из конфигурационного файла приложения. В следующем листинге показан пример конфигурационного файла, который может быть использован, чтобы настроить сборку для языка IronPython с помощью фабричного метода CreateFromConfiguration.

Листинг 8-10: Конфигурационный файл приложения для включения IronPython
<?xml version="1.0"?>
<configuration>
		<configSections>
			<section name="microsoft.scripting"
				type="Microsoft.Scripting.Hosting.Configuration.Section, Microsoft.Scripting"/>
		</configSections>
	<microsoft.scripting>
		<languages>
			<language names="IronPython;Python;py"
				extensions=".py" displayName="IronPython"
				type="IronPython.Runtime.PythonContext, IronPython"/>
		</languages>
	</microsoft.scripting>
</configuration>

Если вы решите использовать фабричный метод на основе файла конфигурации для создания ScriptRuntime, вы должны включить по крайней мере один элемент <language> в коллекцию <languages>.

Для того чтобы убедиться, что загрузка и обработка конфигурационного файла, показанного в листинге 8-10, не прервется и не выбросит исключения во время выполнения, сборки IronPython.dll и Microsoft.Scripting.dll должны быть доступны по закрытому пути сборок в приложении во время выполнения. Но обычно Visual Studio не копирует эти сборки в выходную директорию, потому что, возможно, нет никаких упоминаний о типах из этих сборок в исходном коде C# или Visual Basic. Вы можете избежать этого, установив свойство Copy Local для этих ссылок на True, как показано на рисунке 8-2.

Рисунок 8-2: Visual Studio может не скопировать сборки скриптовых языков, к которым обращаются, в выходную директорию во время компиляции. Установите свойство Copy Local для ссылки на сборку на True в Solution Explorer, как показано здесь, чтобы убедиться, что они будут скопированы.

Когда на Microsoft.Scripting.dll и языковую сборку есть нужные ссылки и доступен файл конфигурации приложения, как показанный в листинге 8-10, следующая строка кода создаст DLR хостинг рантайм без исключений:

ScriptRuntime runtime =
	ScriptRuntime.CreateFromConfiguration();

Вы можете включить в приложение несколько разделов настройки хостинга для более чем одного скриптового языка. Например, вы можете сделать так, чтобы ваша программа одновременно использовала и Python, и Ruby. Это не частое явление, но такое, конечно, возможно. Если вы не хотите использовать файл конфигурации, чтобы установить скриптовый язык, вы можете сделать это в исходном коде, а именно:

var language = new LanguageSetup(
	"IronPython.Runtime.PythonContext, IronPython",
	"IronPython",
	new[] { "IronPython", "Python", "py" },
	new[] { ".py" });
var runtimeSetup = new ScriptRuntimeSetup();
runtimeSetup.LanguageSetups.Add(language);
var runtime = new ScriptRuntime(runtimeSetup);

В этом фрагменте кода класс LanguageSetup построен при помощи значений, аналогичных указанным в элементе <language> файла конфигурации из листинга 8-10. Далее создается класс ScriptRuntimeSetup, и вновь созданный LanguageSetup вставляется в коллекцию LanguageSetups. Наконец, экземпляр ScriptRuntimeSetup передается конструктору класса ScriptRuntime.

Есть также фабричный метод, называемый ReadConfiguration, в ScriptRuntimeSetup, который вы также можете посчитать полезным для настройки рантайм объектов. При помощи этого статического метода конфигурационный файл может быть загружен, чтобы создать ScriptRuntime одной строкой кода:

var customRuntime = new ScriptRuntime(
	ScriptRuntimeSetup.ReadConfiguration(
		"custom.config"));

Вы можете использовать перегруженную версию этого же фабричного метода для загрузки настроек из объекта, наследуемого от System.IO.Stream:

byte[] buffer;
// fill buffer with configuration here
ScriptRuntime streamRuntime;
using (var stream = new MemoryStream(buffer))
{
	streamRuntime = new ScriptRuntime(
		ScriptRuntimeSetup.ReadConfiguration(stream));
}

В этом примере MemoryStream был загружен с конфигурационными данными из какого-то неизвестного источника, может быть, с удаленного конфигурационного сервиса. Результирующий Stream считывается фабричным методом ReadConfiguration, чтобы создать экземпляр ScriptRuntimeSetup, необходимый для построения нового рантайма. Любой тип, наследуемый от Stream, в том числе SqlFileStream или NetworkStream, сделает это удобным фабричным методом для извлечения конфигурационных данных для DLR хостинг рантаймов из различных источников.

Класс ScriptRuntime содержит множество полезных коллекций и вспомогательных методов, описанных на ближайших нескольких страницах. Но мы не можем охватить их все. Для более полной информации скачайте dlr-spec-hosting.pdf с http://dlr.codeplex.com.

Класс ScriptEngine

После получения ссылки на ScriptRuntime, настроенный для языка IronPython, вы можете выполнить файл, содержащий исходник Python, при помощи одной строки кода:

runtimeObject.ExecuteFile("HelloWorld.py");

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

Примечание

На момент написания книги стабильными, рабочими версиями IronPython и IronRuby были v2.7.2.1 и v1.1.3, соответственно. Все примеры в этом разделе книги будут работать с этими версиями. Если вы ссылаетесь на сборку IronPython.dll, вы можете сделать это из Global Assembly Cache (GAC) или непосредственно из папки инсталляции, как правило, из C:\Program Files(x86)\IronPython 2.7 или другого подобного пути. Основная сборка IronRuby устанавливается только в GAC. Но, если нужно, вы можете ссылаться на нее из папки для нужной версии под C:\Windows\Microsoft.NET\Assembly\GAC_MSIL\IronRuby. А еще лучше, загрузите исходный код для IronPython и IronRuby, а затем постройте ваши собственные сборки для этих языков.

Для более точного управления лучше получить ссылку на объект движка, который может выполнить исходный код различными полезными способами. Поскольку один ScriptRuntime можете использовать несколько скриптовых языков одновременно, вы должны запросить конкретный ScriptEngine по имени или по типу файла. Далее показана загрузка языков IronPython и IronRuby в один ScriptRuntime, а затем использование двух различных методов для получения ScriptEngine для каждого языка.

Листинг 8-11: MultiLanguageLoad: получение ScriptEngine для двух языков
public static void MultiLanguageLoad()
{
	var runtimeSetup = new ScriptRuntimeSetup();
	var pythonSetup = new LanguageSetup(
		typeName: "IronPython.Runtime.PythonContext, IronPython",
		displayName: "IronPython",
		names: new[] { "IronPython", "Python", "py" },
		fileExtensions: new[] { ".py" });
	runtimeSetup.LanguageSetups.Add(pythonSetup);
	var rubySetup = new LanguageSetup(
		typeName: "IronRuby.Runtime.RubyContext, IronRuby",
		displayName: "IronRuby",
		names: new[] { "IronRuby", "Ruby", "rb" },
		fileExtensions: new[] { ".rb" });
	runtimeSetup.LanguageSetups.Add(rubySetup);
	ScriptRuntime runtimeObject =
		new ScriptRuntime(runtimeSetup);
	ScriptEngine pythonEngine =
		runtimeObject.GetEngine("Python");
	pythonEngine.Execute("print 'Hello from Python!'");
	ScriptEngine rubyEngine =
		runtimeObject.GetEngineByFileExtension(".rb");
	rubyEngine.Execute("puts 'Hello from Ruby!'");
}

Пример MultiLanguageLoad в листинге 8-11 начинается с создания объектов LanguageSetup для IronPython и IronRuby, а затем они загружаются в новый ScriptRuntime. Поскольку мы не используем типы из IronPython и IronRuby в компилируемом коде, не забудьте установить свойство Copy Local на True для сборок IronPython.dll и IronRuby.dll в проекте Visual Studio, как показано на рисунке 8-2. Остальные строки кода в примере MultiLanguageLoad демонстрируют, как можно получить ссылки на движок языка, используя методы GetEngine и GetEngineByFileExtension для рантайм объекта. При помощи этих ссылок движка выполнение некоторого Python и Ruby кода оказывается довольно простым делом, как показано на рисунке 8-3.

Рисунок 8-3: Выходные данные примера MultiLanguageLoad из листинга 8-11 показывают, что Python и Ruby могут быть выполнены из одного ScriptRuntime при помощи двух конкретных для определенного языка объектов ScriptEngine.

Как вы видите в примере MultiLanguageLoad, метод ScriptEngine Execute принимает строковый параметр, содержащий текст скрипта, который нужно выполнить. Также метод Execute возвращает результат скрипта как значение System.Object. Более того, в DLR-совместимом языке, как C#, возвращаемый объект также обрабатывается как динамический объект:

string name = pythonEngine.Execute(
	"raw_input('What is your name? ')");

Этот фрагмент запросит у пользователей их имена с помощью Python функции raw_input и вернет строковое значение в качестве динамического объекта. Поскольку тип, возвращаемый Execute, помечен как dynamic, C# компилятор генерирует необходимый код в точке присвоения с целью привести возвращаемое значение к System.String. Принудительное преобразование динамических объектов является удобным, но требует затрат, как правило, в производительности. Что делать, если вы хотите получить строго типизированный результат, чтобы избежать лишних затрат с динамической типизацией в C# коде? Как выясняется, существует перегруженная версия метода Execute, чтобы сделать это. Рассмотрим следующую строку C# кода:

int age = engine.Execute<int>(
	"input('How old are you? ')");

В этом примере дженерик метод, называемый Execute, используется для получения строго типизированного результата. Это выполняется внутри Python скрипта, также заметьте, что вместо функции raw_input используется функция input. Функция input в Python сочетает raw_input, которая возвращает строку, с eval, которая оценивает выражение, предоставленное ей, как новое выражение кода. Поскольку вы ожидаете, что пользователь введет число как ответ на вопрос, Python функция input (eval) обрабатывает его как числовой литерал и компилирует в правильный тип.

Эта рантайм оценка выражения становится ясной, если вы посмотрите на рисунок 8-4, который представляет выходные данные примера ReturnScalarFromScript. Обратите внимание, что возраст, который является ответом на второй вопрос, представляет собой выражение, складывающее два целых числа. Python функция input оценивает это выражение, чтобы получить одно целое число, которое возвращается вызывающему элементу строго типизированным. Полный код для этого примера можно найти в листинге 8-12.

Рисунок 8-4: Метод Execute класса ScriptEngine может быть использован для запуска скрипта и возвращать значение приложению.
Листинг 8-12: ReturnScalarFromScript: эксперимент с ScriptEngine Execute
public static void ReturnScalarFromScript()
{
	var runtimeSetup = new ScriptRuntimeSetup();
	var languageSetup = new LanguageSetup(
		"IronPython.Runtime.PythonContext, IronPython",
		"IronPython", new[] { "Python" }, new[] { ".py" });
	runtimeSetup.LanguageSetups.Add(languageSetup);
	var runtime = new ScriptRuntime(runtimeSetup);
	ScriptEngine engine = runtime.GetEngine("Python");
	string name = engine.Execute(
		"raw_input('What is your name? ')");
	int age = engine.Execute<int>(
		"input('How old are you? ')");
	Console.WriteLine(
		"Wow, {0} is only {1} years old!", name, age);
}

Классы ScriptSource и CompiledCode

Есть еще два типа DLR хостинга, которые полезны при работе с классом ScriptEngine, и это типы ScriptSource и CompiledCode. До сих пор мы показали только передачу строк, содержащих скриптовый код. Но может случиться так, что реальному приложению нужно одновременно загружать десятки скриптов из файлов, баз данных или сетевых потоков. Целесообразно хранить эти активы кода в объектной модели, которая имеет свойства и методы для программного управления кодом. Кроме того, ScriptSource является шлюзовым классом для компиляции скриптов для повторного использования по всему приложению. Для получения ссылки на ScriptSource используйте следующие методы ScriptEngine:

  • CreateScriptSource
  • CreateScriptSourceFromFile
  • CreateScriptSourceFromString

Следующий листинг показывает пример получения ScriptSource из ScriptEngine и его использование, чтобы загрузить скриптовый файл из строки до компиляции и выполнить его.

Листинг 8-13: PassingVariablesToCompiledCode
public static void PassingVariablesToCompiledCode(
	string question, object correctResponse)
{
	var runtimeSetup = new ScriptRuntimeSetup();
	var languageSetup = new LanguageSetup(
		"IronPython.Runtime.PythonContext, IronPython",
		"IronPython", new[] { "Python" }, new[] { ".py" });
	runtimeSetup.LanguageSetups.Add(languageSetup);
	var runtime = new ScriptRuntime(runtimeSetup);
	ScriptEngine engine = runtime.GetEngine("Python");
	ScriptSource source =
		engine.CreateScriptSourceFromString(@"
import Question
import CorrectResponse
input(Question) == CorrectResponse
");
	CompiledCode AskQuestion = source.Compile();
	runtime.Globals.SetVariable("Question", question);
	runtime.Globals.SetVariable(
		"CorrectResponse", correctResponse);
	Console.WriteLine("You chose... {0}",
		AskQuestion.Execute<bool>()
			? "wisely."
			: "poorly");
}

В листинге 8-13 после установки ScriptRuntime для IronPython и получения ScriptEngine ссылки, загружается маленький Python скрипт, состоящий из трех строк, при помощи метода движка CreateScriptSourceFromString. ScriptSource затем компилируется при помощи метода экземпляра, метко названного Compile. Это дает нам объект CompiledCode, который может быть выполнен снова и снова.

Однако обратите внимание, что перед выполнением объекта CompiledCode, используется свойство Globals для ScriptRuntime для внедрения двух переменных в область выполнения, к которой скрипт может получить доступ. В следующем разделе мы более подробно рассмотрим области видимости и выполнения. На данный момент считайте область видимости Globals местом, где хостинг приложение C# может сохранять переменные для скрипта, чтобы получить доступ к ним, когда оно запущено. Две переменные, которые внедрены при помощи метода SetVariable, принимают значения из параметров функции и называются «Question» и «CorrectResponse". Крошечный Python скрипт, загруженный в ScriptSource в листинге 8-13, ссылается на эти же имена:

import Question
import CorrectResponse
input(Question) == CorrectResponse

Необходимы два import, поскольку движок IronPython автоматически не загружает переменные из Globals в контекст выполнения. Эти два выражения import вытягивают переменные, внедренные при помощи метода SetVariable, в локальную область видимости, чтобы оставшийся скрипт мог получить к ним доступ. В следующем разделе при создании экземпляра ScriptScope, выражения import не потребуются, поскольку переменные, которые вы внедряете, уже будут доступны в локальной области видимости.

Что касается оставшейся одной строки скриптового кода из листинга 8-13, помните, что функция input в Python выводит предоставленную строку на консоль, считывает сырые входные данные в ответ и запускает их через функцию eval для компиляции и выполнения их в качестве нового выражения. Наконец, это оцененное выражение сравнивается с правильным (или ожидаемым) ответом для получения булева результата. Последняя строка кода C# в листинге 8-13 выполняет запуск и считывает этот булев ответ от скрипта:

Console.WriteLine("You chose... {0}",
	AskQuestion.Execute<bool>()
		? "wisely."
		: "poorly");

Метод Execute<T> для объекта CompiledCode с именем AskQuestion используется для выполнения скрипта. В данном экземпляре дженерик функция конкретизируется как Execute<bool>, чтобы получить результат сравнения, сделанного в последней строке скрипта Python. Если пользователь корректно отвечает на заданный вопрос, он считается умным (wise). Вы могли бы предоставить небольшой тест по химии, вызвав функцию PassingVariablesToCompiledCode в таком порядке:

PassingVariablesToCompiledCode(
	"Platinum has 6 naturally-occuring " +
	"iostopes. True or False? ", true);
PassingVariablesToCompiledCode(
	"By ascending rank, where does the mass " +
	"of calcium in the Earth's\r\ncrust fall " +
	"as compared to the other elements? ", 5);

Результат вызова функции, как эта, можно увидеть на рисунке 8-5. Обратите внимание, что пользователь дал немного развязные ответы, воспользовавшись тем, что будет использована функция Python input для оценки текста перед сравнением с правильным ответом. Для первого вопроса викторины ответ "1 == 1" оценивается как булево значение True, которое и ожидается. Что касается второго вопроса, то ответ "2 + 3" вычисляется как целочисленное значение 5, которое и является правильным значением для ответа на этот вопрос.

Рисунок 8-5: Вызов функции PassingVariablesToCompiledCode из листинга 8-13 в прямой последовательности для создания маленькой викторины по химии.

Теперь, когда вы увидели, как передавать переменные в скомпилированный скрипт и получить результат, пришло время для усовершенствования кода при помощи локального ScriptScope и других оптимизаций. Вы могли заметить, что код в листинге 8-13 неэффективен, поскольку создаются новый рантайм и экземпляры движка каждый раз, когда вызывается функция. В этом нет необходимости. Кроме того, объекты ScriptSource и CompiledCode создаются заново всякий раз, когда вызывается функция, уничтожая реальную ценность компиляции скриптового кода. Вы исправите все эти проблемы в следующем разделе, как изучите DLR класс ScriptScope.

Класс ScriptScope

Вы уже видели, как вы можете использовать свойство Globals для класса ScriptRuntime, чтобы передать переменные выполняемому скрипту. Однако при обсуждении листинга 8-13 мы не коснулись типа данных свойства Globals: экземпляра ScriptScope из пространства имен Microsoft.Scripting.Hosting. Каждый ScriptRuntime содержит единственный объект типа ScriptScope для управления так называемыми глобальными переменными. Но вы можете создать свои собственные объекты ScriptScope для управления переменными, которые связаны друг с другом в соответствии с общей архитектурой приложения. Чтобы показать, как это работает, в следующем листинге создается экземпляр ScriptScope для управления значениями Question и CorrectResponse, показанными в предыдущем листинге.

Листинг 8-14: PoseQuizQuestion: использование пользовательского ScriptScope
private static ScriptEngine _pythonEngine = null;
private static ScriptEngine PythonEngine
{
	get
	{
		if (_pythonEngine == null)
		{
			var runtimeSetup = new ScriptRuntimeSetup();
			var languageSetup = new LanguageSetup(
					"IronPython.Runtime.PythonContext, IronPython",
					"IronPython", new[] { "Python" }, new[] { ".py" });
			runtimeSetup.LanguageSetups.Add(languageSetup);
			var runtime = new ScriptRuntime(runtimeSetup);
			_pythonEngine = runtime.GetEngine("Python");
		}
		return _pythonEngine;
	}
}
private static CompiledCode _askQuestion = null;
private static CompiledCode AskQuestion
{
	get
	{
		if (_askQuestion == null)
		{
			ScriptSource source =
				PythonEngine.CreateScriptSourceFromString(
				"input(Question) == CorrectResponse");
			_askQuestion = source.Compile();
		}
		return _askQuestion;
	}
}
private static ScriptScope _questionScope = null;
private static ScriptScope QuestionScope
{
	get
	{
		if (_questionScope == null)
		{
			_questionScope =
				PythonEngine.CreateScope();
		}
		return _questionScope;
	}
}
public static void PoseQuizQuestion(
	string question, object correctResponse)
{
	QuestionScope.SetVariable("Question", question);
	QuestionScope.SetVariable("CorrectResponse",
		correctResponse);
	Console.WriteLine("You chose... {0}",
		AskQuestion.Execute<bool>(QuestionScope)
			? "wisely."
			: "poorly");
}

Чтобы очистить код из листинга 8-13, что делает его более эффективным, функция PoseQuizQuestion, показанная в листинге 8-14, использует три свойства, реализованные по отдельности:

  1. PythonEngine: свойство типа ScriptEngine, которое гарантирует, что приложение создает экземпляр только одного движка IronPython.
  2. AskQuestion: свойство типа CompiledCode, которое инкапсулирует концепцию скрипта, требуемую для постановки вопросов и оценки ответов пользователя.
  3. QuestionScope: свойство типа ScriptScope, которое содержит Question, который должен быть задан, и CorrectResponse, который должен быть оценен по отношению к ответу.

С помощью этих отдельных свойств можно довольно легко и эффективно формулировать последовательные вопросы пользователю. Функция PoseQuizQuestion вызывает метод SetVariable для ScriptScope, возвращаемый свойством QuestionScope, чтобы ввести две обязательные переменные в локальную, а не глобальную, область видимости, как было показано ранее. Если область видимости до сих пор не создана, аксессор свойства выполняет необходимое создание экземпляра. Следующий вызов метода Execute<T> должен выглядеть знакомым:

Console.WriteLine("You chose... {0}",
	AskQuestion.Execute<bool>(QuestionScope)
		? "wisely."
		: "poorly");

Вы заметили разницу по сравнению с последней строкой C# кода из листинга 8-13? Там не были переданы параметры в Execute<T>, а в листинге 8-14 аналогичный вызов передает значение, полученное из QuestionScope. Это позволяет, чтобы переменные Question и CorrectResponse, на которые ссылается Python скрипт, были обработаны как локальные, а не глобальные переменные. Соответственно, небольшой скрипт, находящийся в свойстве AskQuestion в листинге 8-14, не включает выражение import, как это делает Python скрипт в листинге 8-13.

Поскольку вы передаете ваш собственный ScriptScope при выполнении скрипта, выражения import больше не требуются. С учетом этих изменений последующие вызовы функции PoseQuizQuestion будут повторно использовать кэшированные объекты ScriptEngine, CompiledCode и ScriptScope, чтобы задать каждый вопрос пользователю. Это гораздо более эффективно, как вы можете себе представить.

В дополнение к методу SetVariable, показанному в данном примере, класс ScriptScope содержит другие полезные методы для управления переменными области видимости:

  • bool ContainsVariable(string name): проверить, существует ли заданная переменная.
  • T GetVariable<T>(string name): выбрать значение указанной переменной как конкретный тип, выбрасывая исключение, если искомое не найдено.
  • dynamic GetVariable(string name): выбрать значение указанной переменной как динамический объект, выбрасывая исключение, если искомое не найдено.
  • IEnumerable<string> GetVariableNames(): пройти циклом по именам всех переменных в области видимости.
  • bool RemoveVariable(string name): исключить указанную переменную из области видимости.
  • bool TryGetVariable<T>(string name, out T value): попытаться выбрать значение указанной переменной как конкретный тип, возвращая true, если оно найдено, или false, если нет.
  • bool TryGetVariable(string name, out dynamic value): попытаться выбрать значение указанной переменной как динамический объект, возвращая true, если оно найдено, или false, если нет.

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

Добавление в приложение механизма правил

Скриптинг поведения приложения полностью открытым способом может быть полезным. Использование DLR хостинга, полностью раскрывая объектную модель программы конечному пользователю, - является возможным способом. Но, как правило, вместо этого более безопасно и более ценно предоставить пользователям некоторую ограниченную, предметно-ориентированную структуру. Рассмотрим, например, программу электронной коммерции, где должны применяться комплексные, постоянно меняющиеся правила для дисконтирования товаров в корзине. Python код, приведенный в листинге 8-9, мог бы стать таким правилом для подобной системы. По сути, правила в листинге 8-9 говорят следующее:

  • Если в корзине есть два или более предмета, относящихся к одежде, применить 5-процентную скидку к этим товарам.
  • Если в корзине есть более пяти предметов, относящихся к одежде, применить 9-процентную скидку к этим товарам.
  • Если в общей сложности в корзине находятся более семи товаров, применить специальную 3-процентную скидку ко всем товарам в корзине, независимо от их категории.

Теперь подумайте о том, как вы могли бы написать данное правило таким способом, чтобы пользователи могли его понять. Вы, наверное, не сможете создать какое-либо XML выражение для этого правила, понятное для людей в отделе мерчендайзинга. Хотя, конечно, можно придумать новый диалект, который более удобен для бизнес пользователей, чем то, что показано в листинге 8-9, и язык Python довольно неплохо для этого подходит. Это тем более верно, если бизнес пользователи используют основы языка Python для написания своих правил: операторы if-else или циклы for, например. В следующем разделе мы покажем вам, как создать механизм общих правил, который позволит правилам, показанным в листинге 8-9, выполняться в ваших приложениях.

Пример коммерческого приложения

Давайте рассмотрим объектную модель, которая содержит типы Cart, LineItem и Product. У пользователя программы есть одна покупательская корзина (Cart), в которую он добавляет товары, когда посещает ваш магазин. Внутри Cart есть ноль или более объектов LineItem, представляющих товары в корзине. У каждого LineItem есть Quantity и Product, а также Discount, которые должны применяться во время проверки.

На рисунке 8-6 показан наш пример программы ECommerceExample, где выполняется правило из листинга 8-9. Обратите внимание, что пять предметов одежды получили 5-процентную скидку, а также ко всем товарам в корзине была применена дополнительная 3-процентная скидка. Это соответствует правилу мерчандайзинга, выраженному в листинге 8-9.

Рисунок 8-6: Запуск примера EcommerceExample. Используется правило, представленное в листинге 8-9. К восьми товарам применяются скидки.

Исходя из этого правила мерчандайзинга, после проверки клиент сэкономит $13.93. Чтобы сделать это возможным, в работу вступают класс UnsafeRuleEngine (частично показанный в листинге 8-15) и соответствующий интерфейс, который называется IRule (листинг 8-16). Код в этом простом примере не совсем небезопасный, но есть лучший, более безопасный способ, который мы покажем вам далее.

Листинг 8-15: Основа класса UnsafeRuleEngine
public class UnsafeRuleEngine
{
	private readonly ScriptRuntimeSetup _runtimeSetup;
	private readonly ScriptRuntime _sharedRuntime;
	private readonly Dictionary<int, RuleContext>
		_rulesContexts;
	private static int _nextHandle = 0;
	private struct RuleContext
	{
		internal IRule Rule;
		internal CompiledCode Code;
		internal ScriptScope SharedScope;
	}
	public UnsafeRuleEngine()
	{
		_runtimeSetup = new ScriptRuntimeSetup();
		_runtimeSetup.LanguageSetups.Add(
			new LanguageSetup(
				"IronPython.Runtime.PythonContext, IronPython",
				"IronPython",
				new[] { "IronPython", "Python", "py" },
				new[] { ".py" }));
		_sharedRuntime =
			new ScriptRuntime(_runtimeSetup);
		_rulesContexts =
			new Dictionary<int, RuleContext>();
	}
}
Листинг 8-16: Интерфейс IRule описывает скрипт для бизнес правила
public interface IRule
{
	string Name { get; set; }
	string Address { get; set; }
	string Body { get; set; }
	string ContentType { get; set; }
	string[] ExpectedReturnValueNames { get; set; }
}

Класс UnsafeRuleEngine, показанный в предпоследнем листинге, определяет частную структуру, называемую RuleContext, которая будет использоваться для управления правилами, выполняемыми внутри движка, скомпилированными версиями кода правил и ссылками на ScriptScope для обмена переменными между DLR хостом и скриптовыми движками. UnsafeRuleEngine поддерживает словарь этих контекстов, чтобы служить в качестве кэша скомпилированного кода скрипта.

Совет

Конечно, можно запустить более одного типа скриптового языка в пределах одного движка правил, как показано ранее в примере MultiLanguageLoad. Для явной простоты в примере из листинга 8-15 показано только добавление языка Python к UnsafeRuleEngine. Чтобы включить другой язык, вам нужно только добавить соответствующий LanguageSetup во время построения механизма правил.

Интерфейс IRule, показанный в листинге 8-16, служит в качестве основы для любого бизнес правила.

Каждое правило, выполняемый UnsafeRuleEngine, должен соответствовать данному стандарту:

  • Name: имя правила (используется исключительно для удобства)
  • Address: имя файла, ключ базы данных или имя пользователя, которые инициируют правило
  • Body: исходный код для правила
  • ContentType: тип кода, выраженного в Body (Python, Ruby и так далее)
  • ExpectedReturnValueNames: массив имен переменных, для которых механизм правил должен выбрать связанные значения после выполнения правила

UnsafeRuleEngine сохраняет ссылки на вновь добавленные правила, используя два метода, показанные в следующем листинге. Эти методы вставки и обновления используют закрытый вспомогательный метод UpsertRule, показанный в следующем листинге.

Листинг 8-17: Методы InsertRule и UpdateRule класса UnsafeRuleEngine
public int InsertRule(IRule rule)
{
	int handle = -1;
	UpsertRule(rule, ref handle);
	return handle;
}
public void UpdateRule(int handle, IRule rule)
{
	UpsertRule(rule, ref handle);
}
private void UpsertRule(IRule rule,
	ref int handle)
{
	CompiledCode compilation = null;
	ScriptScope sharedScope = null;
	ScriptEngine engine = _sharedRuntime
		.GetEngineByFileExtension(
			rule.ContentType);
	sharedScope = engine.CreateScope();
	ScriptSource source = engine
		.CreateScriptSourceFromString(
			rule.Body);
	compilation = source.Compile();
	if (_rulesContexts.ContainsKey(handle))
		_rulesContexts.Remove(handle);
	else
		handle = System.Threading
			.Interlocked.Increment(
				ref _nextHandle);
	_rulesContexts[handle] =
		new RuleContext()
		{
			Rule = rule,
			Code = compilation,
			SharedScope = sharedScope,
		};
}

Когда правило вставлено, вызывающему элементу возвращается обработка целых чисел. Эта обработка позже может быть использована для обновления правила. Метод UpsertRule – это то место, где происходит вся реальная работа. После того как свойство правила ContentType обнаруживает соответствующий скриптовый язык, движок компилирует код и сохраняет его в новом RuleContext, наряду с общим ScriptScope для использования во время будущих расширений правила. Наконец, вам нужно иметь возможность выполнить правила в кэше UnsafeRuleEngine.

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

Листинг 8-18: Метод Execute класса UnsafeRuleEngine
public IDictionary<string, dynamic> Execute(
	int handle, IDictionary<string, object> parameters)
{
	RuleContext context =
		_rulesContexts[handle];
	ScriptScope scope = context.SharedScope;
	foreach (var kvp in parameters)
		scope.SetVariable(kvp.Key, kvp.Value);
	context.Code.Execute(scope);
	var results = new Dictionary<string, dynamic>();
	if (context.Rule.ExpectedReturnValueNames != null
		&& context.Rule.ExpectedReturnValueNames.Length > 0)
	{
		dynamic result;
		foreach (var valueName in
			context.Rule.ExpectedReturnValueNames)
		{
			if (valueName == null
				|| valueName.Trim().Length == 0)
			{
				continue;
			}
			if (scope.TryGetVariable(
				valueName.Trim(), out result))
			{
				results.Add(valueName, result);
			}
		}
	}
	return results;
}

Метод Execute также принимает словарь именованных параметров для вставки в ScriptScope перед запуском правила.

Обратите внимание, что метод Execute возвращает словарь именованных динамических объектов на основе имен, выраженных в свойстве ExpectedReturnValueNames в контексте правила.

Исходный код

Полный исходный код к ECommerceExample, обсуждаемому здесь, довольно большой, поэтому мы решили напечатать только самые актуальные его части. Чтобы получить полный исходный код, посетите http://metadotnetbook.codeplex.com и откройте примеры главы 8.

После загрузки правила мерчендайзинга, показанного в листинге 8-11, в переменную discountRule, унаследованную от IRule, следующий небольшой фрагмент C# кода запустит правило внутри любого .NET объекта, который реализует коллекцию правильно структурированных LineItems:

var engine = new UnsafeRuleEngine();
var ruleHandle = engine.InsertRule(discountRule);
engine.Execute(
	handle: ruleHandle,
	parameters: new Dictionary<string, object>{{"cart", this}});

Обратите внимание, что parameters, переданные правилу, содержат одну пару имя-значение, которая называется cart. Значением параметра является this, а это обозначает, что объектом, выполняющим код, должна быть так называемая покупательская корзина, которая содержит LineItems со свойствами Quantity, Discount и Product. Каждый из этих объектов Product также должен реализовать свойство Category. Как только эти условия будут удовлетворены, правило будет работать, как и ожидалось. Вернитесь к листингу 8-11, и вы увидите, что Python скрипт обращается к переменной cart таким способом. Python скрипт будет обновлять свойство Discount каждого LineItem, основываясь на мерчендайзинговых предпочтениях его автора.

Более безопасный пример приложения

Теперь, когда вы знаете, как это просто – выполнить бизнес правило на основе Python с произвольным .NET объектом, мы, к сожалению, должны немного все усложнить. Проблема с UnsafeRuleEngine заключается в том, что он дает слишком много полномочий выполнению скриптов. С правилом по скидке, показанным в листинге 8-11, все в порядке, потому что оно только обновляет только Discount для товаров в корзине. Но что, если бы поведение было не таким правильным? Что, если скрипт изменил свойство Price каждого Product таким образом, что компания теряет деньги при каждой сделке? Не касаясь вопросов, связанных с полномочиями скрипта, что если почти одновременное выполнение этого же скрипта вставило другой объект cart в общий ScriptScope в самый неподходящий момент?

Хостинговая модель DLR предлагает три уровня изоляции для решения этих проблем. На самом высоком уровне каждое событие выполнения может быть присвоено разным ScriptScope. Это позволит одному ScriptEngine изолировать переменные, используемые при выполнении, от всех других переменных, которые могут работать в это же время. Изоляция области видимости не решит проблему блуждающего скрипта, описанную ранее, но это может решить некоторые другие проблемы. Программист может выбрать стек DLR хостинга для создания нового ScriptRuntime, каждый раз, когда выполняется скриптовый код. Это было бы крайне неэффективно, так что вы могли бы вместо этого кэшировать ScriptRuntime для отдельного правила. Это могло бы решить тот же набор проблем, которые решаются изоляцией области видимости и, возможно, некоторые другие, когда каждому правилу дается собственное время выполнения. Но изоляция рантайма по-прежнему позволяет скриптам менять любые объекты, к которым они получают доступ таким способом, который может быть не целесообразным.

Хостинговая модель DLR предоставляет третий тип изоляции, который может решить проблему блуждающего скрипта. Как показано на рисунке 8-7, программист может создать домен приложения, который разделяет среду исполнения скрипта от среды выполнения хост приложения.

Рисунок 8-7: Выполнение скрипта в отдельном AppDomain

Различные .NET объекты, которые будут внедрены в сферу видимости скрипта, должны быть сериализованы через границу AppDomain или расположены по порядку как ссылочные типы. Следующий листинг показывает полный класс RuleEngine для замены UnsafeRuleEngine, описанного ранее.

Листинг 8-19: Более безопасный (потенциально) класс RuleEngine
using System;
using System.Dynamic;
using System.Collections.Generic;
using Microsoft.Scripting.Hosting;
namespace DevJourney.Scripting
{
	public enum IsolationMode { Shared, Private }
	public class RuleEngine
	{
		private readonly ScriptRuntimeSetup _runtimeSetup;
		private readonly ScriptRuntime _sharedRuntime;
		private readonly Dictionary<int, RuleContext> _rulesContexts;
		private readonly AppDomain _remoteAppDomain;
		private static int _nextHandle = 0;
		private struct RuleContext
		{
			internal IRule Rule;
			internal CompiledCode Code;
			internal ScriptScope SharedScope;
			internal bool IsIsolatedRuntime;
		}
		public RuleEngine(IsolationMode appDomainMode)
		{
			_runtimeSetup = new ScriptRuntimeSetup();
			_runtimeSetup.LanguageSetups.Add(
				new LanguageSetup(
					"IronPython.Runtime.PythonContext, IronPython",
					"IronPython",
					new[] { "IronPython", "Python", "py" },
					new[] { ".py" }));
			if (appDomainMode == IsolationMode.Private)
				_remoteAppDomain = AppDomain.CreateDomain(
					DateTime.UtcNow.ToString("s"));
			_sharedRuntime =
				(_remoteAppDomain != null)
					? ScriptRuntime.CreateRemote(
						_remoteAppDomain, _runtimeSetup)
					: new ScriptRuntime(_runtimeSetup);
			_rulesContexts =
				new Dictionary<int, RuleContext>();
		}
		public IRule SelectRule(int handle)
		{
			lock (_rulesContexts)
			{
				if (!_rulesContexts.ContainsKey(handle))
					throw new ArgumentOutOfRangeException(
						"handle", 
						String.Format("The rule " +
							"context with handle {0} cannot be " +
							"selected from the cache because it " +
							"does not exist.", handle));
				return _rulesContexts[handle].Rule;
			}
		}
		public int InsertRule(IRule rule,
		IsolationMode runtimeMode)
		{
			lock (_rulesContexts)
			{
				int handle = -1;
				UpsertRule(rule, ref handle, runtimeMode);
				return handle;
			}
		}
		public void UpdateRule(int handle, IRule rule,
		IsolationMode runtimeMode)
		{
			lock (_rulesContexts)
			{
				if (!_rulesContexts.ContainsKey(handle))
					throw new ArgumentOutOfRangeException(
						"handle",
						String.Format("The rule " +
							"context with handle {0} cannot be " +
							"updated in the cache because it " +
							"does not exist.", handle));
				UpsertRule(rule, ref handle, runtimeMode);
			}
		}
		public void DeleteRule(int handle)
		{
			lock (_rulesContexts)
			{
				if (!_rulesContexts.ContainsKey(handle))
					throw new ArgumentOutOfRangeException(
						"handle",
						String.Format("The rule " +
							"context with handle {0} cannot be " +
							"deleted from the cache because it " +
							"does not exist.", handle));
				if (_rulesContexts[handle].IsIsolatedRuntime)
				{
					_rulesContexts[handle].Code.Engine
						.Runtime.Shutdown();
				}
				_rulesContexts.Remove(handle);
			}
		}
		public IDictionary<string, dynamic> Execute(
			int handle,
			IDictionary<string, object> parameters,
			IsolationMode scopeMode)
		{
			RuleContext context;
			lock (_rulesContexts)
			{
				if (!_rulesContexts.ContainsKey(handle))
					throw new ArgumentOutOfRangeException(
						"handle",
						String.Format("Rule handle " +
							"{0} was not found in the rule cache.",
							handle));
			context = _rulesContexts[handle];
		}
		ScriptScope scope =
			(scopeMode == IsolationMode.Private)
				? context.Code.Engine.CreateScope()
				: context.SharedScope;
		foreach (var kvp in parameters)
			scope.SetVariable(kvp.Key, kvp.Value);
		context.Code.Execute(scope);
		var results = new Dictionary<string, dynamic>();
		if (context.Rule.ExpectedReturnValueNames != null
			&& context.Rule.ExpectedReturnValueNames.Length > 0)
		{
			dynamic result;
			foreach (var valueName in context.Rule.ExpectedReturnValueNames)
			{
				if (valueName == null
					|| valueName.Trim().Length == 0)
				{
					continue;
				}
				if (scope.TryGetVariable(
					valueName.Trim(), out result))
				{
					results.Add(valueName, result);
				}
			}
			return results;
		}
		private void UpsertRule(IRule rule,
		ref int handle, IsolationMode runtimeMode)
		{
			if (rule == null)
				throw new ArgumentNullException("rule");
			lock (_rulesContexts)
			{
				CompiledCode compilation = null;
				ScriptScope sharedScope = null;
				ScriptRuntime runtime =
					(runtimeMode == IsolationMode.Private)
						? (_remoteAppDomain != null)
							? ScriptRuntime.CreateRemote(
								_remoteAppDomain,
								_runtimeSetup)
							: new ScriptRuntime(_runtimeSetup)
						: _sharedRuntime;
				ScriptEngine engine = runtime
					.GetEngineByFileExtension(rule.ContentType);
				sharedScope = engine.CreateScope();
				ScriptSource source = engine
					.CreateScriptSourceFromString(rule.Body);
				compilation = source.Compile();
				if (_rulesContexts.ContainsKey(handle))
					DeleteRule(handle);
				else
					handle = System.Threading
						.Interlocked.Increment(ref _nextHandle);
				_rulesContexts[handle] =
					new RuleContext()
					{
						Rule = rule,
						Code = compilation,
						SharedScope = sharedScope,
						IsIsolatedRuntime =
							(runtimeMode == IsolationMode.Private)
					};
			}
		}
	}
}

Подчеркивая различия между UnsafeRuleEngine и новым классом RuleEngine, первое, на что нужно обратить внимание, это существование перечисляемого типа IsolationMode. Значения этого типа могут быть использованы в классе RuleEngine, чтобы включить все три режима изоляции, которые поддерживаются хостинговой моделью DLR. Первая и самая важная из них – это изоляция AppDomain, которую можно включить, передав IsolationMode.Private конструктору RuleEngine. Это заставляет RuleEngine создать новый AppDomain от имени хост приложения, в котором будут выполнены все правила, вставленные в RuleEngine.

Следующий тип DLR хостинг изоляции, который может быть включен с помощью класса RuleEngine, это изоляция ScriptRuntime. Чтобы сделать это, передайте IsolationMode.Private как параметр runtimeMode при вызове методов InsertRule или UpdateRule. Это приведет к тому, что конкретное правило будет запущено в собственном ScriptRuntime каждый раз, когда оно выполняется. Поймите, однако, изоляция рантайма отдельна и независима от AppDomain изоляции. Вы можете форсировать создание отдельного AppDomain, а затем, например, использовать общий ScriptRuntime для всех правил, которые работают в нем. Вы также можете запустить несколько правил в локальном AppDomain, но разделить их выполнение, используя различные рантаймы. Вы свободны в выборе модели изоляции в соответствии с вашими потребностями.

Новый класс RuleEngine также поддерживает изоляцию ScriptScope. Для включения этого передайте IsolationMode.Private в качестве параметра scopeMode при вызове метода Execute. Изоляция области видимости ортогональна к обеим изоляциям AppDomain и ScriptRuntime. С новым классом RuleEngine вы вольны выбрать комбинацию всех трех стратегий изоляции, которая имеет смысл для вашего приложения.

Маршалинг и сериализация .NET объектов через ScriptScope

Класс RuleEngine разрешает выполнение правил в отдельном AppDomain, что гораздо безопаснее, чем разрешить им работать внутри AppDomain хост программы. Но позволить правилам работать удаленно – это полдела. Как показано на рисунке 8-7, есть два способа подготовить объекты для передвижения или использования через границы AppDomain. В ECommerceExample вы можете унаследовать класс Cart от MarshalByRefObject вот так:

public class Cart : MarshalByRefObject

После вызова метода SetVariable класса ScriptScope, чтобы внедрить объект Cart в область выполнения, код DLR Hosting API сможет маршализировать объектную ссылку в удаленный AppDomain. Скрипт, запущенный здесь, будет иметь доступ к объекту Cart, как если бы экземпляр был создан локально. Все хорошо работает, но это по-прежнему позволяет скрипту изменять свойства товаров в корзине, к которым он не должен иметь доступ. Кроме того, если ваши доменные объекты уже имеют базовый класс, который не может быть маршалируемым, это решение может не подойти вам.

Другой способ перемещать .NET объекты Cart назад и вперед через границы AppDomain заключается в том, чтобы пометить их как [Serializable], например:

[Serializable] public class Cart

Это также позволяет DLR Hosting API копировать Cart в удаленный AppDomain для доступа скрипта. Слово копировать здесь – это ключ к пониманию основных различий между методом MarshalByRefObject и методом [Serializable] для установки и извлечения переменных в ScriptScope. Когда объект [Serializable] Cart вставляется в ScriptScope, его копия доступна в удаленном AppDomain. Если скрипт меняет Cart любым способом, то будет изменена только копия, а не оригинал.

Предстоит сделать гораздо больше работы при использовании такой семантики «передать по значению» для ваших .NET типов. Некоторые члены этих типов не могут быть сериализованы должным образом, так что вам придется пометить их как [NonSerialized], чтобы удержать их от «пересечения границ» AppDomain. Кроме того, вам нужно будет извлечь измененный Cart из ScriptScope, когда завершится выполнение правила. Наконец, поскольку измененный Cart является копией, вам нужно сравнить его с тем, который был передан перед исполнением, чтобы посмотреть, какие изменения были сделаны. В следующем листинге вы увидите два новых метода Cart, которые делают эту работу: CompareModifiedLineItem и UpdateFromModifiedCart.

Листинг 8-20: Методы для сравнения и обновления модифицированной покупательской корзины
private bool CompareModifiedLineItem(
	LineItem original, LineItem modified, int ndx)
{
	if (modified == null)
		throw new ApplicationException(
			String.Format("After recalculating " +
				"the cart value, line item {0} was " +
				"null. The discount script must not " +
				"remove line items.", ndx));
	if (modified.Product == null)
		throw new ApplicationException(
			String.Format("After recalculating " +
				"the cart value, the product on " +
				"line {0} was null. The discount " +
				"script must not modify the " +
				"products.", ndx));
	if (!original.Product.Equals(modified.Product))
		throw new ApplicationException(
			String.Format("After recalculating " +
				"the cart value, the product on " +
				"line {0} was different from the " +
				"original. The discount script " +
				"must not modify the products.", ndx));
	if (original.Quantity != modified.Quantity)
		throw new ApplicationException(
			String.Format("After recalculating " +
				"the cart value, the quantity on " +
				"line {0} was different from the " +
				"original. The discount script " +
				"must not modify quantities.", ndx));
	return (original.Discount != modified.Discount);
}
private void UpdateFromModifiedCart(
	Cart modifiedCart)
{
	if (modifiedCart == null)
		throw new ApplicationException(
			"The modified cart was not returned " +
			"from the discount script as expected.");
	if (LineItems.Length !=
		modifiedCart.LineItems.Length)
		throw new ApplicationException(
		String.Format("After recalculating the " +
			"cart value, {0} line items were " +
			"expected but {1} items were found. " +
			"The discount script must not add " +
			"or remove line items.", LineItems.Length,
			modifiedCart.LineItems.Length));
	for (int ndx = 0; ndx < LineItems.Length; ndx++)
	{
		LineItem original = LineItems[ndx];
		LineItem modified =
			modifiedCart.LineItems[ndx];
		if (CompareModifiedLineItem(
			original, modified, ndx))
		{
			original.Discount = modified.Discount;
		}
	}
}

Метод UpdateFromModifiedCart следует взывать непосредственно после выполнения скидка правила о скидке. Это позволит убедиться, что скрипт не пытается модифицировать другие свойства, чем каждое LineItem Discount. Такие проверки безопасности являются обязательными, но мы настоятельно рекомендуем ловить ошибки в скриптах на этапе подготовки и отладки. После того как проверка безопасности завершена, допускается, чтобы только измененные свойства Discount были обновлены в исходных объектах Cart LineItems. Возможно, вы понимаете, почему изоляция AppDomain при помощи объектов [Serializable] – это самый безопасный способ для интеграции механизма правил в ваше .NET приложение. Полный исходный код для ECommerceExample доступен на http://metadotnetbook.codeplex.com. Он содержит полностью рабочие классы RuleEngine и Cart, которые демонстрируют изоляцию AppDomain, ScriptRuntime и ScriptScope, а также выборочное использование семантики «передать-по-ссылке» и «передать-по-значению».