Project

Profile

Help

Support #4290

closed

Custom .NET Function

Added by Kevon Hayes over 4 years ago. Updated over 4 years ago.

Status:
Closed
Priority:
Normal
Category:
.NET API
Sprint/Milestone:
-
Start date:
2019-08-20
Due date:
% Done:

0%

Estimated time:
Legacy ID:
Applies to branch:
9.9
Fix Committed on Branch:
Fixed in Maintenance Release:
Platforms:

Description

I would like to create a custom .NET extension to be used by my XSLTs. Your documentation shows how to create the C# code, but your C# code does not compile.

Also, there are no examples showing the namespace being registered in a stylesheet nor is there an example showing how the method is to be called.

Can you please specify examples for these? My end goal is to create file functions to overcome not being able to call file:base-dir()


Files

SampleXslt.xsl (1.01 KB) SampleXslt.xsl Kevon Hayes, 2019-08-21 21:22
Actions #1

Updated by O'Neil Delpratt over 4 years ago

  • Category set to .NET API
  • Assignee set to O'Neil Delpratt
Actions #2

Updated by O'Neil Delpratt over 4 years ago

Hi,

We will investigation the example in the documentation and report back here.

Please see below the example of a extension function for the maths square function on a number:

(nunit test case)

 [Test]
        public void TestSqrt() {
            string p =
       @"<xsl:package name='http://www.ctk.cz/math' " +
       @"  xmlns:xsl='http://www.w3.org/1999/XSL/Transform' xmlns:xs='http://www.w3.org/2001/XMLSchema' version='3.0' " +
       @"  xmlns:math='http://www.ctk.cz/math' " +
       @"  xmlns:demo='http://example.math.co.uk/demo'>" +
       @"  <xsl:function name='math:sqrt' as='xs:double?' visibility='final'>" +
       @"     <xsl:param name='num' as='xs:double?'/>" +
       @"     <xsl:sequence select='demo:sqrt($num)'/>" +
       @"  </xsl:function>" +
       @"</xsl:package>";


            String s = @"<xsl:transform version='3.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'" +
                      @" xmlns:math='http://www.ctk.cz/math' " +
                      @" exclude-result-prefixes='math'> " +
                      @" <xsl:use-package name='http://www.ctk.cz/math'/>" +
                      @" <xsl:template name='xsl:initial-template' > " +
                      @" <out " +
                      @" sqrt2='{math:sqrt(2.0e0)}' " +
                      @" sqrtEmpty='{math:sqrt(())}'> " +
                      @" </out> " +
                      @" </xsl:template></xsl:transform>";

            try
            {

                Processor processor = new Processor(true);
                processor.RegisterExtensionFunction(new Sqrt());
                processor.SetProperty("http://saxon.sf.net/feature/optimizationLevel", "-f");
                XsltCompiler xsltCompiler = processor.NewXsltCompiler();
                xsltCompiler.XsltLanguageVersion = "3.0";
                var ms = new MemoryStream(Encoding.UTF8.GetBytes(p));
                XsltPackage pp = xsltCompiler.CompilePackage(ms);
                xsltCompiler.ImportPackage(pp);
                XsltExecutable exec = xsltCompiler.Compile(new StringReader(s));
                Xslt30Transformer transf = exec.Load30();
                XdmDestination dest = new XdmDestination();
                transf.CallTemplate(null, dest);
                Console.WriteLine(dest.XdmNode);
                Assert.True(true, "ok");
            }
            catch (Exception exc) {
                Assert.Fail(exc.Message);
            }
        }

        public class Sqrt : ExtensionFunctionDefinition
        {
            public override QName FunctionName { get { return new QName("http://example.math.co.uk/demo", "sqrt"); } }

            public override int MinimumNumberOfArguments
            {
                get
                {
                    return 1;
                }
            }

            public override int MaximumNumberOfArguments
            {
                get
                {
                    return 1;
                }
            }

            public override XdmSequenceType[] ArgumentTypes
            {
                get
                {
                    return new XdmSequenceType[]{
                new XdmSequenceType(XdmAtomicType.BuiltInAtomicType(QName.XS_DOUBLE), '?')
            };
                }
            }

            public override XdmSequenceType ResultType(XdmSequenceType[] ArgumentTypes)
            {
                return new XdmSequenceType(XdmAtomicType.BuiltInAtomicType(QName.XS_DOUBLE), '?');
            }

            public override bool TrustResultType
            {
                get
                {
                    return true;
                }
            }


            public override ExtensionFunctionCall MakeFunctionCall()
            {
                return new SqrtCall();
            }
        }

        internal class SqrtCall : ExtensionFunctionCall
        {
            public override IXdmEnumerator<XdmItem> Call(IXdmEnumerator<XdmItem>[] arguments, DynamicContext context)
            {
                Boolean exists = arguments[0].MoveNext();
                if (exists)
                {
                    XdmAtomicValue arg = (XdmAtomicValue)arguments[0].Current;
                    double val = (double)arg.Value;
                    double sqrt = System.Math.Sqrt(val);
                    XdmAtomicValue result = new XdmAtomicValue(sqrt);
                    return (IXdmEnumerator<XdmItem>)((IXdmEnumerable<XdmItem>)result).GetEnumerator();
                }
                else
                {
                    return EmptyEnumerator<XdmItem>.INSTANCE;
                }
            }
        };
Actions #3

Updated by Kevon Hayes over 4 years ago

You registering 2 namespaces?

http://example.math.co.uk/demo on the QName instantiation and http://www.ctk.cz/math

Do I need both?

Neither of these translate to how this page: https://www.saxonica.com/html/documentation/extensibility/dotnetextensions/ says to call the method. e.g. xmlns:OS="clitype:System.OperatingSystem"

where OS is the prefix and clitype:System.OperatingSystem is the fully qualified type name. Fully qualified for meaning C# Namespace + Class name.

namespace Company.Business.SaxonExtensions
{
	public class C365SqrtDefinition: ExtensionFunctionDefinition
	{
		public override QName FunctionName
		{
			get { return new QName("http://math.com/", "sqrt"); }
		}

		public override int MinimumNumberOfArguments
		{
			get { return 1; }
		}

		public override int MaximumNumberOfArguments
		{
			get { return 1; }
		}

		public override XdmSequenceType[] ArgumentTypes
		{
			get
			{
				return new XdmSequenceType[]
				{
					new XdmSequenceType(XdmAtomicType.BuiltInAtomicType(QName.XS_DOUBLE), '?')
				};
			}	
		}

		public override XdmSequenceType ResultType(XdmSequenceType[] ArgumentTypes)
		{
			return new XdmSequenceType(XdmAtomicType.BuiltInAtomicType(QName.XS_DOUBLE), '?');
		}

		public override bool TrustResultType
		{
			get { return true; }
		}

		public override ExtensionFunctionCall MakeFunctionCall()
		{
			return new C365SqrtCall();
		}
	}

	public class C365SqrtCall : ExtensionFunctionCall
	{

		public override IXdmEnumerator<XdmItem> Call(IXdmEnumerator<XdmItem>[] arguments, DynamicContext context)
		{
			Boolean exists = arguments[0].MoveNext();
			if (exists)
			{
				XdmAtomicValue arg = (XdmAtomicValue)arguments[0].Current;
				double val = (double)arg.Value;
				double sqrt = System.Math.Sqrt(val);
				XdmAtomicValue result = new XdmAtomicValue(sqrt);
				return (IXdmEnumerator<XdmItem>)((IXdmEnumerable<XdmItem>)result).GetEnumerator();
			}
			else
			{
				return EmptyEnumerator<XdmItem>.INSTANCE;
			}
		}
	}
}

Considering the above code what would my XSLT namespaces be?

Actions #4

Updated by O'Neil Delpratt over 4 years ago

I had two namespace because I also introduced the xsl:function math:sqrt. The xsl:function is not required, the stylesheet could have designed differently.

Actions #5

Updated by O'Neil Delpratt over 4 years ago

In my example the demo prefix relates to the namespace http://example.math.co.uk/demo which is for the extension function.

Actions #6

Updated by Kevon Hayes over 4 years ago

From your documentation I understand my namespace can be like the following

xmlns:c365Fn="clitype:Company.Business.SaxonExtensions.C365SqrtDefinition" or xmlns:c365Fn="clitype:Company.Business.SaxonExtensions.C365SqrtCall" or xmlns:c365Fn="http://math.com/"

and my function call can be like:

<xsl:variable name="my-variable" select="c365Fn:sqrt(64)"/>

Can you clarify?

Documentation I found: https://www.saxonica.com/html/documentation/extensibility/dotnetextensions/ https://www.saxonica.com/html/documentation/extensibility/dotnetextensions/staticmethods.net.html

Actions #7

Updated by Kevon Hayes over 4 years ago

Does my internal namespace and class name dictate the function name or does the below code dictate the function? public override QName FunctionName { get { return new QName("http://example.math.co.uk/demo", "sqrt"); } }

Actions #8

Updated by Michael Kay over 4 years ago

For reflexive extension functions, the namespace is related to the class containing the method that you want to call. (My experience of reflexive extension functions in .NET is that it's very hard to get them working properly, because of all the complexities of dynamic loading with strong names, etc: but if you know .NET inside out that probably isn't a problem for you)

For "integrated extension functions", you can choose any namespace you like and it has no relation to the class name of the implementation.

When looking for examples, do check the saxon-resources download file.

Actions #9

Updated by Kevon Hayes over 4 years ago

Michael,

So the question that still remains is: Can I call .NET extension methods from my XSLT files?

Actions #10

Updated by Debbie Lockett over 4 years ago

Yes, you can call .NET extension methods from your XSLT. As Mike said, there are two ways of doing this: "reflexive extension functions" (as documented at the pages you found http://www.saxonica.com/html/documentation/extensibility/dotnetextensions/ ) and "integrated extension functions" (as in the example O'Neil provided, which can also be found in the documentation at http://www.saxonica.com/html/documentation/extensibility/integratedfunctions/ext-fns-N.html). For more information about using integrated extension functions, see the documentation section http://www.saxonica.com/html/documentation/extensibility/integratedfunctions/

(Examples can also be found in the C# samples files in the saxon-resources download under samples/cs/ e.g. see XsltIntegratedExtension in ExamplesPE.cs)

Actions #11

Updated by Kevon Hayes over 4 years ago

Debbie,

O'Neil example assumes I'm going to pull my xsl in as a string, convert it as a memory stream and then compile it. Michael and I already went over doing this way our xsl includes are not found. This way is NOT what I would deem as using being able to call my .NET extensions from it because it is all in imperative code.

So enter reflexive extensions. This way seems like what I would want. So the question I have with Reflexive extensions is... using ExtensionFunctionDefinition and ExtensionFunctionCall is not needed with this method correct?

Actions #12

Updated by Debbie Lockett over 4 years ago

O'Neil's example only demonstrates one way of calling an integrated extension function from XSLT. You do not have to supply the XSLT as a string this way. But the key is to register the integrated extension function:

"Having written an integrated extension function, it must be registered with Saxon so that calls on the function are recognized by the parser. This is done using the registerExtensionFunction method available on the Configuration class, and also on the s9api Processor class. It can also be registered via an entry in the configuration file."

(Above is quoted from the bottom of page http://www.saxonica.com/html/documentation/extensibility/integratedfunctions/ext-full-J.html)

So the key line in O'Neil's example is

processor.RegisterExtensionFunction(new Sqrt());

The way the XSLT is supplied is just for the purpose of that example. Whatever mechanism you are using or have in mind to use with reflexive extension functions would also be possible with integrated extension functions, as long as they are registered with Saxon.

Correct, for reflexive extension functions, you do not need to use ExtensionFunctionDefinition and ExtensionFunctionCall.

Actions #13

Updated by Kevon Hayes over 4 years ago

Debbie,

This was my code when trying to implement extension functions and I receive a cannot find function error from Saxon. Let me know if something was omitted.

// Declaring
namespace Comply365.Business.SaxonExtensions
{
	public class Sqrt: ExtensionFunctionDefinition
	{
		public override QName FunctionName
		{
			get { return new QName("http://saxonextensions.comply365.com/", "sqrt"); } 
		}

		public override int MinimumNumberOfArguments
		{
			get { return 1; }
		}

		public override int MaximumNumberOfArguments
		{
			get { return 1; }
		}

		public override XdmSequenceType[] ArgumentTypes
		{
			get
			{
				return new XdmSequenceType[]
				{
					new XdmSequenceType(XdmAtomicType.BuiltInAtomicType(QName.XS_DOUBLE), '?')
				};
			}	
		}

		public override XdmSequenceType ResultType(XdmSequenceType[] ArgumentTypes)
		{
			return new XdmSequenceType(XdmAtomicType.BuiltInAtomicType(QName.XS_DOUBLE), '?');
		}

		public override bool TrustResultType
		{
			get { return true; }
		}

		public override ExtensionFunctionCall MakeFunctionCall()
		{
			return new SqrtCall();
		}
	}

	public class SqrtCall : ExtensionFunctionCall
	{

		public override IXdmEnumerator<XdmItem> Call(IXdmEnumerator<XdmItem>[] arguments, DynamicContext context)
		{
			Boolean exists = arguments[0].MoveNext();
			if (exists)
			{
				XdmAtomicValue arg = (XdmAtomicValue)arguments[0].Current;
				double val = (double)arg.Value;
				double sqrt = System.Math.Sqrt(val);
				XdmAtomicValue result = new XdmAtomicValue(sqrt);
				return (IXdmEnumerator<XdmItem>)((IXdmEnumerable<XdmItem>)result).GetEnumerator();
			}
			else
			{
				return EmptyEnumerator<XdmItem>.INSTANCE;
			}
		}
	}
}

// Parsing

var saxonConfigPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data\\licenses\\COMPLY365-SAXON.config");
var saxonConfigFile = new FileStream(saxonConfigPath, FileMode.Open);
 
_processor = new Processor(saxonConfigFile, new Uri(saxonConfigPath));
 
_processor.RegisterExtensionFunction(new Sqrt());
XPathCompiler xpc = _processor.NewXPathCompiler();
xpc.DeclareNamespace("c365Fn", "http://saxon-extensions.comply365.com/");

// And this was my XSL

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:xd="http://www.oxygenxml.com/ns/doc/xsl" xmlns:fo="http://www.w3.org/1999/XSL/Format"
                xmlns:axf="http://www.antennahouse.com/names/XSL/Extensions"
                xmlns:rev="http://www.boeing.com/FTID-ML/Revision" xmlns:exsl="http://exslt.org/common"
                xmlns:ftid="http://www.boeing.com/FTID-ML" exclude-result-prefixes="xd" version="3.0"
                xmlns:file="http://expath.org/ns/file"
                xmlns:c365Fn="http://saxon-extensions.comply365.com/"
                xmlns:pn="http://internal-project-namespace">

<xsl:variable name="whatIsTheSquareRoot" select="c365Fn:sqrt(64)"/>
<fo:block font-size="12pt" text-align="left">
        <xsl:value-of select="descendant::ftid:PUInfo/@sectionName"/>
        <xsl:value-of select="$whatIsTheSquareRoot"/>                      
</fo:block>
</xsl:stylesheet>

(edited by MHK to correct the Markdown formatting)

Actions #14

Updated by Kevon Hayes over 4 years ago

The only thing that is off I see now is the namespaces in the ExtensionFunctionDefinition vs registering the function with the processor. http://saxonextensions.comply365.com/ vs http://saxon-extensions.comply365.com/

I'm testing this now to see if it matters at all.

Actions #15

Updated by Debbie Lockett over 4 years ago

Could you try again to attach the XSLT stylesheet? That part of comment #13 is missing.

I also don't see in the C# code where you are actually using the Processor to run any XSLT transform. Where is that?

Actions #16

Updated by Kevon Hayes over 4 years ago

Attached xslt as file attachment.

// My call to compile is 
public void Transform(string inputPath, string stylesheetPath, string outputPath)
		{
			try
			{
				var transformer = _xsltCompiler.Compile(new Uri(stylesheetPath)).Load();
                        }
               }
Actions #17

Updated by Kevon Hayes over 4 years ago

That was the issue. The namespace declared on the FunctionDefinition has to be the same as the namespace used when registering the function with the Processor.

For us .NETers I thinks this buys some us some functionality. Although processing takes noticeably longer but not still adequate.

Actions #18

Updated by Debbie Lockett over 4 years ago

Glad to hear you got the integrated .NET extension function to work. Yes, both the namespace and name of the registered function must match. Unfortunately this is an easy mistake to make.

Actions #19

Updated by Kevon Hayes over 4 years ago

Support,

Question: Can the "Call" method of an integrated extension method just return an XdmItem rather than an IXdmEnumerator ? Or must it be an IXdmEnumerator if so why?

Actions #20

Updated by Michael Kay over 4 years ago

ExtensionFunction.Call returns XdmValue and XdmItem is a subtype of XdmValue, so yes, you can return an XdmItem.

The documentation of the method appears to be incorrect; it has apparently been copied from ExtensionFunctionCall. If I remember correctly, the ExtensionFunction class was introduced as a simplification of the rather complex (and richer) ExtensionFunctionDefinition/ExtensionFunctionCall combo, and one of the changes we made was that it always returns a value rather than an iterator. Of course this is less efficient if you want to return a sequence of a million items, but that's an unusual requirement.

So for simple extension functions you can implement the ExtensionFunction interface and return any XdmValue including for example an XdmNode or an XdmAtomicValue. If you have more complex requirements, for example if you need access to the static or dynamic context or if you are returning large values, you should implement ExtensionFunctionDefinition/ExtensionFunctionCall, and in that case you need to return an iterator.

I'm slighltly surprised to see that XdmValue.GetEnumerator() returns IEnumerator<XdmItem> rather than IXdmEnumerator. That feels to me like a bug.

Actions #21

Updated by Michael Kay over 4 years ago

  • Status changed from New to Closed
  • Priority changed from High to Normal

This issue spun off a couple of documentation and usability bugs, e.g. #4296, which are being tracked separately. Other than that, I think the questions raised are now resolved, so I am closing the tracker.

Please register to edit this issue

Also available in: Atom PDF