Форум программистов, компьютерный форум, киберфорум
diadiavova
Войти
Регистрация
Восстановить пароль
Рейтинг: 5.00. Голосов: 1.

Возвращение поддержки внедренных скриптов в XSLT в .Net Core и .Net 5

Запись от diadiavova размещена 29.05.2021 в 09:00

  1. Немного теории
  2. Процессор
  3. Добавление статического объекта
  4. Компиляция
  5. ProxyGenerator
  6. Результаты
В .Net Framework для XSLT поддерживалась возможность включать в XSLT скрипты, написанные на нескольких языках программирования (C#, VB.Net, JScript.Net). Функции, описанные в этих скриптах можно использовать в выражениях XPath, благодаря чему возможности языка (довольно скромные, если использовать только стандарты) возрастают многократно. В следующих версиях XSLT (тех, что выше первой) многое поменялось и необходимость в их расширении возникает уже не так часто, но в .Net более поздние версии XSLT не поддерживаются и реализация такой поддержки в стандартных библиотеках не планируется. Использование внешних инструментов имеет свои ограничения, по крайней мере для бесплатных версий. Поддержка же возможности расширения языка собственным кодом, написанным на упомянутых языках, доводит возможности XSLT до такого уровня, что его хватает для решения практически любых задач.
Добавлять свои функции в преобразование можно двумя способами: функции можно описать в самой программе, выполняющей преобразование; и в скриптах, размещенных непосредственно в документе XSLT. Возможности, которые дают оба способа – одинаковы. По этой причине может показаться, что внедренные скрипты не очень-то и нужны. Но объекты расширения, добавляемые в коде программы должны быть описаны самой программе и предварительно скомпилированы. Таким образом, если возникнет необходимость добавить какую-то новую функцию, придется внести ее в код программы. Ну и соответственно, в этом случае придется заранее позаботиться о том, чтобы расширение обладало достаточно большим набором функций. В то же время преимущество XSLT, на мой взгляд, как раз в том и состоит, что этот язык позволяет вынести логику трансформации документа за пределы программного кода. В этом смысле удаление поддержки скриптов – достаточно серьезная потеря, которую хотелось бы как-то компенсировать.

Немного теории

За преобразование XSLT у нас отвечает класс XslCompiledTransform. Об использовании внедренных скриптов можно почитать здесь Использование скриптов таблиц стилей XSLT<msxsl:script>. Добавление объектов расширения в коде программы осуществляется с помощью класса XsltArgumentList.
Внедренный скрипт фактически представляет из себя код класса без самой декларации класса и фактически это действует ровно также, как и при использовании объектов расширения, где объект должен содержать некоторый набор методов, которые и будут доступны в выражениях XPath. В тоже самое время XSLT является словарем XML, благодаря чему у нас есть возможность его предварительной обработки, используя при этом инструменты работы с XML. Таким образом, если мы хотим, чтобы выполнялись скрипты, нам для этого нужно в процессе загрузки XSLT извлечь из него все скрипты, сформировать на их основе классы, скомпилировать результат, создать экземпляры классов и в процессе преобразования добавить их XsltArgumentList как объекты расширения. Собственно, этим мы сейчас и будем заниматься.

Процессор

Класс, отвечающий за выполнение преобразования должен быть неким подобием класса XsltCompiledTransform. То есть в нем должны быть методы Load и Transform. Это важно из-за того, что в процессе загрузки документа XSLT будет выполняться некоторая предварительная обработка. В частности, мы будем извлекать скрипты и компилировать их. Динамическая компиляция занимает некоторое время и если обрабатывается только один документ, то можно процессы загрузки и собственно преобразования объединить в один метод. Но мы будем исходить из того, что бывают случаи, когда одно преобразование используется для целого пакета документов и в этом случае секунды, потраченные на компиляцию нужно будет умножить на количество документов в пакете, что конечно же уже будет занимать немало времени. В то же время, если мы будет выполнять компиляцию скриптов при загрузке, то при выполнении преобразования у нас уже все будет скомпилировано.

Поскольку код скрипта не является полноценным кодом, готовым к компиляции, для начала нам потребуется из кода скрипта создать полноценный код класса. Поскольку мы хотим реализовать поддержку максимально возможного количества языков, нам придется позаботиться о том, чтобы код формировался правильно независимо от языка. Удобнее всего для этого создать дерево кода из объектов CodeDom. Итак, добавляем в код процессора метод, который будет на основе скрипта, представленного XElement, создавать код класса.
VB.NET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
   Private Function GenerateCode(script As XElement) As String
        Dim provider = cdc.CodeDomProvider.CreateProvider(script.@language)
        Dim unit As New cd.CodeCompileUnit
        Dim ns1 As New cd.CodeNamespace("Ns1")
        unit.Namespaces.Add(ns1)
        Dim class1 As New cd.CodeTypeDeclaration("Class1")
        ns1.Types.Add(class1)
 
        Dim defaultns = $"System,System.Collections,System.Text,System.Text.RegularExpressions,System.Xml,System.Xml.Xsl,System.Xml.XPath".
            Split(","c, StringSplitOptions.RemoveEmptyEntries).
            Concat(script.<msxsl:using>.Select(Function(ns) ns.@namespace)).Distinct().
            Select(Function(ns) New CodeDom.CodeNamespaceImport(ns))
        ns1.Imports.AddRange(defaultns.ToArray)
        If "vb visualbasic".Split(" "c).Contains(script.@language.ToLower) Then
            ns1.Imports.Add(New CodeDom.CodeNamespaceImport("Microsoft.VisualBasic"))
        End If
        Dim codeText = New cd.CodeSnippetTypeMember(script.Value)
        class1.Members.Add(codeText)
        Dim sb As New StringBuilder
        Using writer As New IO.StringWriter(sb)
            provider.GenerateCodeFromCompileUnit(unit, writer, New CodeDom.Compiler.CodeGeneratorOptions())
        End Using
        Return sb.ToString
    End Function
На что здесь следует обратить внимание. Класс, созданный этим методом будет всегда иметь имя Ns1.Class1. Это не будет иметь значения в том случае, если сборка имеет только один этот класс. Если же мы захотим собрать по всему документу все скрипты и скомпилировать их в одну сборку, это приведет к конфликту имен и в этом случае логику именования придется изменить.
Второй момент: по умолчанию я добавил несколько пространств имен. Когда внедренные скрипты поддерживались там происходило ровно то же самое. То есть там предусмотрена возможность импортировать любые пространства имен, но даже без этого несколько пространств импортируется без этого. Естественно этого можно либо вообще не делать, либо изменить этот список по своему усмотрению.
Далее нам следует позаботиться о том, чтобы из каждого скрипта документа создавался объект расширения и добавлялся в список таких объектов. Добавление XsltArgumentList можно осуществить так же как это делается в XsltCompiledTransform, то есть добавить несколько перегрузок метода Transform, содержащих параметр этого типа. Но мне показалось, что будет более удобно создавать этот объект автоматически при загрузке XSLT, добавлять в него все что нужно и при выполнении преобразования использовать этот объект. Но на случай, если нужно будет еще и «вручную» добавить что-то в этот список, нужно будет предоставить доступ к этому объекту. Таким образом нам понадобится следующее свойство
VB.NET
1
2
3
4
5
6
7
    Dim _ArgumentList As XsltArgumentList
 
    Public ReadOnly Property ArgumentList As XsltArgumentList
        Get
            Return _ArgumentList
        End Get
    End Property
Теперь о методе, который будет добавлять объекты в список. Сам метод будет принимать объект скрипта (XElement), далее его задача извлечь код, сформировать на основе этого кода и других данных код для компиляции с помощью метода, о котором написано выше, скомпилировать его, создать объект, определить пространство имен, с которым он будет ассоциирован и добавить это все дело в список аргументов. И вот здесь при реализации всего этого дела я столкнулся с еще одной проблемой. Дело в том, что в .Net Framework задача динамической компиляции решалась с помощью того же CodeDomProvider, но в .Net 5 и .Net Core такая возможность не поддерживается. То есть необходимые методы у этого класса вроде как есть, но они не реализованы. По этой причине для компиляции пришлось использовать инструменты Roslyn, ниже я опишу процесс, сейчас же я об этом написал, чтобы было понятно, что за объекты будут использоваться в коде. Метод, который здесь будет вызван для компиляции кода у меня возвращает кортеж (EmitResult, Assembly, String). Первый элемент этого кортежа содержит информацию, возвращенную компилятором, второй – скомпилированную сборку, третий – код, который был скомпилирован. С первыми двумя все понятно, но может возникнуть вопрос, зачем понадобилось сохранять код. Тут объяснение простое: обычно информация компилятора содержит данные о положении ошибки в коде. Но у нас кода как такового нет, он формируется «на лету» и положение ошибки не будет соответствовать ее положению в XSLT-документе, где она и будет расположена на самом деле. Все это будет сохраняться в специальном свойстве
VB.NET
1
2
3
4
5
6
    Dim _CompilationResultList As New List(Of (EmitResult, Assembly, String))
    Public ReadOnly Property CompilationResultList As List(Of (EmitResult, Assembly, String))
        Get
            Return _CompilationResultList
        End Get
    End Property
Ну и собственно сам код метода
VB.NET
1
2
3
4
5
6
7
8
9
10
11
    Private Sub AddExtObj(script As XElement)
        Dim refs = script.<msxsl:assembly>.Where(Function(asm) asm.@href IsNot Nothing).Select(Function(asm) asm.@href)
        Dim libpath = IO.Path.GetDirectoryName(GetType(String).Assembly.Location)
        refs.Concat(script.<msxsl:assembly>.Where(Function(asm) asm.@name IsNot Nothing).Select(Function(asm) Path.Combine(libpath, asm.@name)))
        Dim cresult = Compiler.CompileCode(GenerateCode(script), script.@language, refs.ToArray)
        _CompilationResultList.Add(cresult)
        If Not cresult.Item1.Success Then Exit Sub
        Dim obj = Activator.CreateInstance(cresult.Item2.GetType("Ns1.Class1"))
        Dim xmlns = script.GetNamespaceOfPrefix(script.Attribute("implements-prefix"))
        _ArgumentList.AddExtensionObject(xmlns.NamespaceName, obj)
    End Sub
Естественно, для того, чтобы все правильно искалось нужно импортировать необходимые пространства имен, в частности в этом коде используется следующее
VB.NET
1
Imports <xmlns:msxsl="urn:schemas-microsoft-com:xslt">
Добавление статического объекта

Кроме того, ради чего мы все это дело затеяли (возвращение поддержки внедренных скриптов), можно добавить еще пару-тройку возможностей, которые тоже не худо было бы иметь. На базе всего того, кода, что мы сейчас напишем это сделать совсем несложно.
Приведу простой пример. Помимо тех функций, которые мы могли бы написать с помощью скриптов или объектов расширения, неплохо было бы иметь возможность включать в скрипты уже существующие функции, которые изначально не предполагалось использовать в XSLT. Для примера можно взять математический функции, которых в первой версии XSLT так не хватает. Доступные нам «из коробки» математические функции определены в классе System.Math. И, казалось бы, что мешает использовать все это богатство напрямую. Но проблема в том, XsltArgumentList принимает объект, а вышеозначенный класс не позволяет создавать экземпляры, а все его методы – статические (Shared). То же самое можно сказать и о модулях Visual Basic, которые также содержать массу полезных функций.
Решить данную проблему можно выполнив следующие действия:
Сначала нам потребуется придумать собственное пространство имен для наших расширений. Можно, конечно, использовать и msxsl, добавив в него собственный функционал, но лучше этого не делать, дабы избежать возможных конфликтов. Я в своем примере добавил в XSLT вот такое пространство имен
Код:
xmlns:my="urn:my-xslt-extension-object-generator"
Затем включил в документ вот такой элемент
XML
1
<my:static-object assembly-name="System.Runtime.dll" type="System.Math" implements-prefix="math"/>
Элемент содержит информацию о сборке типе и префиксе. Префикс мы будем использовать для вызова функций, таким образом нам понадобится объявить еще пространство имен для этого объекта расширения и ассоциировать его с этим префиксом.
Код:
xmlns:math="urn:my-xslt-extension-object-generator:math"
Пространство не обязательно должно быть именно таким, а вот префикс должен совпадать со значением атрибута implements-prefix. Если объектов будет несколько, то для функций каждого из них нужно будет создать свое пространство имен и вызывать их через свой префикс.
Теперь о коде. Во время загрузки документа нам потребуется обойти все эти элементы и обработать каждый из них специальным методом.
VB.NET
1
2
3
4
5
6
7
8
9
10
11
12
13
    Private Sub AddStaticObject(saticObj As XElement)
        Dim asmpath = Path.GetDirectoryName(GetType(System.Object).Assembly.Location)
        Dim asmFileName = saticObj.@<assembly-name> & If(saticObj.@<assembly-name>.EndsWith(".dll"), "", ".dll")
        Dim refs = New String() {If(saticObj.@<assembly-href>, Path.Combine(asmpath, asmFileName))}
        Dim proxygen As New ProxyGenerator
        Dim code = proxygen.GenerateStaticMethodsProxy(Assembly.LoadFile(refs(0)).GetType(saticObj.@type))
        Dim cresult = Compiler.CompileCode(code, "vb", refs) 
        _CompilationResultList.Add(cresult)
        If Not cresult.Item1.Success Then Exit Sub
        Dim obj = Activator.CreateInstance(cresult.Item2.GetType((saticObj.@type).Split("."c).Last & "StaticProxy"))
        Dim xmlns = saticObj.GetNamespaceOfPrefix(saticObj.@<implements-prefix>)
        _ArgumentList.AddExtensionObject(xmlns.NamespaceName, obj)
    End Sub
Что здесь происходит. Для того, чтобы иметь возможность добавлять объект, нам потребуется прокси-класс, который мы создадим динамически. Для генерации кода класса мы создадим специальный класс ProxyGenerator, именно он здесь и используется. Код класса, который мы сгенерируем с его помощью будет: иметь возможность создавать экземпляры, содержать только (или в основном) методы, которые можно использовать в XSLT с его сильно ограниченной системой типов, а методы будут просто переадресовывать вызовы соответствующему статическому классу.
В своем проекте я других вариантов расширений не создавал, но так, если подумать, то, наверно, неплохо было бы еще создать расширение, которое позволило бы загружать динамически объект расширения из указанной сборки. Реализовать это несложно, но здесь я это описывать уже не буду.
Load и Transform
Метод Load у нас должен выполнять несколько действий. Во-первых, он должен сохранить документ XSLT, что вполне понятно. Во-вторых, следует обойти все скрипты и обработать их методом AddExtObj. В-третьих, то же самое нужно сделать со статическими объектами, равно как и с другими типами расширений, которые будут добавлены. Ну и наконец, нам нужно будет убедиться, что компиляция всех кодов прошла успешно, а в противном случае вызвать исключение.
Кроме того, нам понадобится поле для хранения загруженного документа
VB.NET
1
   Dim xslt As XDocument
VB.NET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    Public Sub Load(xsltPath As String)
        xslt = XDocument.Load(xsltPath)
        _ArgumentList = New XsltArgumentList
        _CompilationResultList.Clear()
        For Each script As XElement In xslt...<msxsl:script>
            AddExtObj(script)
        Next
        For Each stat As XElement In xslt...<my:static-object>
            AddStaticObject(stat)
        Next
 
        If Not _CompilationResultList.All(Function(r) r.Item1.Success) Then
            Throw New Exception($"Во время компиляции внедренных скриптов произошла были выявлены ошибки. 
Подробности в списке {NameOf(CompilationResultList)} объекта {NameOf(XsltProcessor)}, вызвавшего исключение.")
        End If
    End Sub
Также желательно реализовать несколько перегрузок этого метода, но это уже чисто для удобства вызова.
Для метода Transform также неплохо было бы реализовать несколько перегрузок, наподобие тех, какие имеет класс XsltCompiledTransform. Но я здесь тоже покажу минимум того, что нужно.
VB.NET
1
2
3
4
5
6
7
    Public Sub Transform(input As XmlReader, output As TextWriter)
        Dim ctransform As New XslCompiledTransform(True)
        Dim cleandoc = New XDocument(xslt)
        cleandoc.Root.<msxsl:script>.Remove
        ctransform.Load(cleandoc.CreateReader)
        ctransform.Transform(input, _ArgumentList, output)
    End Sub
Здесь мы из сохраненного документа создаем копию и из этой копии нужно удалить все скрипты. Несмотря на то, что внедренные скрипты здесь не поддерживаются, XsltCompiledTransform знает о них и при добавлении объекта расширения, ассоциированного с тем же пространством имен, что и скрипт, он вываливает ошибку. Поэтому скрипты и нужно удалить. А вот удалять то, что мы сами придумали необязательно, поскольку неизвестные элементы здесь просто игнорируются. Хотя, на всяких случай можно сделать и это.

Компиляция

Как уже говорилось выше, для компиляции нам понадобятся инструменты Roslyn. Есть несколько пакетов NuGet, которые можно использовать, но для наших целей удобнее всего будет пакет Microsoft.CodeAnalysis.Compilers, он содержит компиляторы для обоих поддерживаемых языков (C# и VB.Net, если нужна поддержка других, придется искать другие инструменты) в одном пакете и ничего лишнего.
Код приведу полностью
VB.NET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
Imports System.IO
Imports System.Runtime.Loader
Imports Microsoft.CodeAnalysis
Imports Microsoft.CodeAnalysis.CSharp
Imports cs = Microsoft.CodeAnalysis.CSharp
Imports Microsoft.CodeAnalysis.Text
Imports Microsoft.CodeAnalysis.VisualBasic
Imports vb = Microsoft.CodeAnalysis.VisualBasic
Imports System.Reflection
Imports Microsoft.CodeAnalysis.Emit
Imports System.Xml.XPath
Imports System.Xml
 
Public Class Compiler
 
    Private Shared Function GetCompilation(srcText As SourceText, lang As String, asmName As String, refs() As String) As Compilation
        Dim result As Compilation
        Dim reflist = refs.Select(Function(af) MetadataReference.CreateFromFile(af))
 
        Select Case lang.ToLower
            Case "vb", "visualbasic"
                Dim tree = vb.SyntaxFactory.ParseSyntaxTree(srcText)
                result = VisualBasicCompilation.Create(asmName, New SyntaxTree() {tree}, references:=reflist,
options:=New VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary,
                                           optimizationLevel:=OptimizationLevel.Release,
                                           assemblyIdentityComparer:=AssemblyIdentityComparer.Default))
            Case "cs", "csharp", "c#"
                Dim tree = cs.SyntaxFactory.ParseSyntaxTree(srcText)
                result = CSharpCompilation.Create(asmName, New SyntaxTree() {tree}, references:=reflist,
options:=New CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary,
                           optimizationLevel:=OptimizationLevel.Release,
                           assemblyIdentityComparer:=AssemblyIdentityComparer.Default))
            Case Else
                Throw New NotSupportedException($"{lang} - Неизвестное определение языка. Поддерживаемые определения: vb, visualbasic, cs, csharp, c#")
        End Select
        Return result
    End Function
 
    Public Shared Function CompileCode(code As String, lang As String, references As IEnumerable(Of String)) As (EmitResult, Assembly, String)
        Dim asmreturn As Reflection.Assembly
        Using stream As New MemoryStream
            Dim srcCode = SourceText.From(code)
            Dim refNames = "System.Runtime System.Text.RegularExpressions System.Xml.XDocument System.Xml.ReaderWriter".Split(" "c)
            Dim refs = New List(Of String) From {
                Assembly.GetExecutingAssembly().Location,
                GetType(Object).Assembly.Location,
                GetType(XPathNodeIterator).Assembly.Location
            }
 
            refs.AddRange(references)
            refs.AddRange(refNames.Select(Function(astr) Assembly.Load(astr).Location))
            Dim compilation = GetCompilation(srcCode, lang, "temp.dll", refs.Distinct().ToArray)
            Dim result = compilation.Emit(stream)
            If (Not result.Success) Then
                Return (result, Nothing, code)
            End If
            stream.Seek(0, SeekOrigin.Begin)
            Dim asmLoadContext = New AssemblyLoadContext("temp.dll")
            asmreturn = asmLoadContext.LoadFromStream(stream)
            Return (result, asmreturn, code)
        End Using
    End Function
End Class
Здесь опять-таки, набор «референсов», загружаемый по умолчанию можно отредактировать по усмотрению или вынести это дело в конфигурацию проекта.

ProxyGenerator

При генерации кода прокси класса не имеет значения на каком языке его генерировать, поскольку результат не будет содержать пользовательского кода. И тут вроде нет особой необходимости использовать CodeDom для генерации, а вместо этого можно использовать простую генерацию текста. Но когда я попробовал сделать так, то с классом System.Math проблем не было, но вот когда я попытался тем же способом добавить модуль Microsoft.VisualBasic.Strings, возникли проблемы. Например ошибку вызвала вот эта функция Asc(String), все дело в том, что параметр этой функции имеет имя String, а это имя является ключевым словом языка и если оно используется в качестве идентификатора, должно заключаться в квадратные скобки. Таким образом получается, что для генерации нам понадобится как минимум список таких слов и при использовании имен придется выполнять их проверку. Если же использовать генерацию на основе CodeDom, эти проблемы будут решены автоматически, да и вообще мало ли какие еще проблемы могли бы возникнуть.
Итак, нам нужно создать класс, в котором для каждого статического метода будут формироваться что-то подобное. Если исходный код выглядит так
VB.NET
1
Public Function Asc (String As String) As Integer
То результат должен быть следующим
VB.NET
1
2
3
    Public Function Asc([String] As String) As Integer
        Return Microsoft.VisualBasic.Strings.Asc([String])
    End Function
И так для каждого метода.
Помимо этого, нам нужно будет решить еще пару задач. Нам необходимо будет отфильтровать методы класса, поскольку далеко не все что может быть в CLR-типах пригодно для использования в XSLT. В первую очередь проблема в системах типов. XSLT поддерживает ограниченный набор типов. Для первой версии (с которой мы и работаем) это: числа, строки, «булины», узлы и наборы узлов (кажись все). Узлы в коде функций будут представлены типом System.Xml.XPath.XPathNavigator , а наборы узлов - System.Xml.XPath.XPathNodeIterator. Точнее производными классами, поскольку эти абстрактны. Естественно нас интересуют только те функции, у которых типы параметров и возвращаемого значения совместимы с данным набором. Это могут быть примитивы, строки, перечисления и вышеозначенные типы, представляющие узлы и их наборы. Таким образом в оригинальных типах нам нужно будет отфильтровать методы.
Другой момент, это то, что среди методов, возвращающих «неудобные» типы могут оказаться и довольно полезные. Ну например - в том же модуле Strings есть довольно полезная функция Split, реализация которой средствами XSLT – та еще проблема. Но вот что потом делать с результатом? В XSLT первой версии нет средств для работы с массивом строк, то есть его потом просто не будет возможности разобрать на составляющие. И вот для таких полезных методов можно предусмотреть конвертеры. В частности, коллекции строк можно заменять наборами текстовых узлов, с которыми уже можно работать.
Код генератора:
VB.NET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
Imports System.CodeDom
Imports System.Reflection
Imports System.Text
Imports System.Xml.XPath
Imports eo = IncludedScriptSupport.ExtensionObjects
Public Class ProxyGenerator
 
    Private Function GenerateMethodDom(mi As MethodInfo) As CodeMemberMethod
        Dim result As New CodeMemberMethod()
        With result
            .Name = mi.Name
            .Attributes = MemberAttributes.Public
            .Parameters.AddRange(
            New CodeParameterDeclarationExpressionCollection(
                mi.GetParameters().
                Select(Function(pi) New CodeParameterDeclarationExpression(pi.ParameterType, pi.Name)).ToArray))
            .ReturnType = New CodeTypeReference(If(mi.ReturnType.IsAssignableTo(GetType(IEnumerable(Of String))), GetType(XPathNodeIterator), mi.ReturnType))
            Dim typeref = New CodeTypeReferenceExpression(mi.DeclaringType)
            Dim method As New CodeMethodReferenceExpression(typeref, mi.Name)
            Dim params = mi.GetParameters().Select(Function(pi) New CodeVariableReferenceExpression(pi.Name))
            Dim inv As New CodeMethodInvokeExpression(method, params.ToArray)
            If mi.ReturnType.IsAssignableTo(GetType(IEnumerable(Of String))) Then
                inv = AddStringListConverter(inv)
            End If
 
            .Statements.Add(New CodeMethodReturnStatement(inv))
        End With
        Return result
    End Function
 
    Private Function AddStringListConverter(expr As CodeMethodInvokeExpression) As CodeMethodInvokeExpression
        Dim method As New CodeMethodReferenceExpression(New CodeTypeReferenceExpression(GetType(eo.Helpers)), NameOf(eo.Helpers.SListToNodeset))
        Dim result As New CodeMethodInvokeExpression(method, expr)
        Return result
    End Function
 
    Private Function GenerateProxyClassDom(t As Type) As CodeTypeDeclaration
        Dim types = New Type() {
            GetType(String),
            GetType(XPathNavigator),
            GetType(XPathNodeIterator),
            GetType(IEnumerable(Of String)),
            GetType([Enum])}
        Dim methods = t.GetMethods(BindingFlags.Public Or BindingFlags.Static).
            Where(Function(mi)
                      Return mi.GetParameters().Select(Function(pi) pi.ParameterType).Append(mi.ReturnType).
                      All(Function(mt) mt.IsPrimitive OrElse types.Any(Function(ct) mt.IsAssignableTo(ct)))
                  End Function)
        Dim typedecl As New CodeTypeDeclaration(t.Name & "StaticProxy")
        typedecl.Members.AddRange((From mi As MethodInfo In methods Select GenerateMethodDom(mi)).ToArray)
        Return typedecl
    End Function
 
    Public Function GenerateStaticMethodsProxy(t As Type) As String
        Dim sb As New StringBuilder
        Using writer As New IO.StringWriter(sb)
            Dim provider = CodeDom.Compiler.CodeDomProvider.CreateProvider("VB")
            provider.GenerateCodeFromType(GenerateProxyClassDom(t), writer, Nothing)
        End Using
        Return sb.ToString()
    End Function
End Class
По поводу конвертера. Собственно, сам конвертер можно либо внедрить в сборку, либо реализовать в другой сборке, но в этом случае при компиляции кода надо будет дать ссылку на эту сборку. Я реализовал конвертер в самом проекте и при компиляции добавляю ссылку на него.
VB.NET
1
2
3
4
5
6
7
8
9
10
11
Imports System.Xml.XPath
 
Namespace ExtensionObjects
    Public Class Helpers
        Public Shared Function SListToNodeset(slist As IEnumerable(Of String)) As XPathNodeIterator
            Return <r><%= From ss As String In slist Select <a><%= ss %></a> %></r>.ToXPathNavigable().CreateNavigator().Select(".//text()")
        End Function
 
    End Class
 
End Namespace
Здесь я импортировал пространство System.Xml.XPath для того, чтобы были доступны определенные в этом пространстве методы расширения для XNode, нас интересует здесь метод ToXPathNavigable(), чтобы получить навигатор для узла и получить набор узлов с помощью метода Select. То есть здесь я просто создал фрагмент документа, содержащий строки коллекции и выбрал их как узлы.
Результаты
Приложение я сделал предельно простым: на форме кнопка и текстовое поле. Нажимаем на кнопку и выполняется преобразование и в текстовом поле появляется либо результат, либо список ошибок, если все прошло неудачно. Код кнопки такой
VB.NET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Dim proc As New XsltProcessor()
        Dim input = XDocument.Load(IO.Path.Combine(Application.StartupPath, "XMLFile1.xml"))
        Try
            proc.Load(IO.Path.Combine(Application.StartupPath, "XSLTFile1.xslt"))
        Catch ex As Exception
            TextBox1.Text = ex.ToString
            For Each res In proc.CompilationResultList
                TextBox1.AppendText(res.Item3)
                TextBox1.AppendText(String.Join(vbCrLf & vbCrLf, res.Item1.Diagnostics.Select(Function(d) d.ToString)))
            Next
            Exit Sub
        End Try
        Dim sb As New StringBuilder
        Using writer = New IO.StringWriter(sb)
            proc.Transform(input.CreateReader, writer)
        End Using
        TextBox1.Text = (sb.ToString)
    End Sub
Тестировалось все на следующем примере. Входной документ
XML
1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8" ?>
<input>
    <pow base="5" exp="3"/>
    <dblstr>ma</dblstr>
    <mid start="9" length="5">This is input string</mid>
    <split>string1,string2,string3</split>
</input>
XSLT
XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl s1 s2 my math str"
xmlns:s1="urn:included-script:s1"
xmlns:s2="urn:included-script:s2"
xmlns:my="urn:my-xslt-extension-object-generator"
xmlns:math="urn:my-xslt-extension-object-generator:math"
xmlns:str="urn:my-xslt-extension-object-generator:strings"
                                >
    <xsl:output method="xml" indent="yes"/>
 
    <xsl:template match="@* | node()">
        <xsl:copy>
            <xsl:apply-templates select="@* | node()"/>
        </xsl:copy>
    </xsl:template>
 
    <xsl:template match="/">
        <result>
            <pow>
                <xsl:value-of select="s1:pow(*/pow/@base, */pow/@exp)"/>
            </pow>
            <dblstr>
                <xsl:value-of select="s2:dblstr(*/dblstr/text())"/>
            </dblstr>
            <exp>
                <xsl:value-of select="math:Exp(1)"/>
            </exp>
            <mid>
                <xsl:value-of select="str:Mid(*/mid/text(), */mid/@start, */mid/@length)"/>
            </mid>
            <split>
                <xsl:for-each select="str:Split(*/split/text(), ',', -1, 1)">
                    <fragment>
                        <xsl:value-of select="."/>
                    </fragment>
                </xsl:for-each>
            </split>
        </result>
    </xsl:template>
 
    <my:static-object assembly-name="System.Runtime.dll" type="System.Math" implements-prefix="math"/>
    <my:static-object assembly-name="Microsoft.VisualBasic.Core" type="Microsoft.VisualBasic.Strings" implements-prefix="str"/>
 
    <msxsl:script implements-prefix="s1" language="visualbasic">
        <![CDATA[
        Public Function pow(base As Double, exp As Double)
            Return Math.Pow(base, exp)
        End Function
        ]]>
    </msxsl:script>
 
    <msxsl:script implements-prefix="s2" language="csharp">
        <![CDATA[
        public string dblstr(string input)
        {
            return input + input;
        }
        ]]>
    </msxsl:script>
 
</xsl:stylesheet>
Здесь мы видим в коде преобразования два скрипта, на двух поддерживаемых языках, кроме того два импортированных статических объекта (Math и Strings) и само преобразование использует функции как из скриптов, так и из импортированных объектов.
Результат:
XML
1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-16"?>
<result>
  <pow>125</pow>
  <dblstr>mama</dblstr>
  <exp>2.718281828459045</exp>
  <mid>input</mid>
  <split>
    <fragment>string1</fragment>
    <fragment>string2</fragment>
    <fragment>string3</fragment>
  </split>
</result>
>>
Вложения
Тип файла: zip IncludedScriptSupport.zip (6.98 Мб, 78 просмотров)
Размещено в Без категории
Показов 570 Комментарии 0
Всего комментариев 0
Комментарии
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2021, vBulletin Solutions, Inc.