在X++中编译并执行C#脚本

发生了什么?

这是件非常有趣的事情。我们现在可以在X++中编译并执行C#脚本。请看下面的X++代码:

static void runCSharp(Args _args)
{
    System.Collections.ArrayList                        scriptArgs;
    System.Collections.ArrayList                        returns;
    System.Collections.IEnumerator                      enumerator;
    System.Collections.Specialized.StringCollection     errors;
    SunnyChen.CSharpScript.ScriptRunner                 runner;
    int                                                 result;

    #localmacro.SourceScript
        "using System;" +
        "using System.Collections;" +
        "public class Script" +
        "{" +
        "    public static ArrayList EntryMethod(ArrayList args)" +
        "    {" +
        "        int a = (int)args[0];" +
        "        int b = (int)args[1];" +
        "        int c = a + b;\r\n" +
        "        ArrayList returns = new ArrayList();" +
        "        returns.Add(c);" +
        "        return returns;" +
        "    }" +
        "}"
    #endmacro

    ;
    runner = new SunnyChen.CSharpScript.ScriptRunner();
    // Prepares the parameters
    scriptArgs = new System.Collections.ArrayList();
    scriptArgs.Add(10);
    scriptArgs.Add(20);

    // Runs the script
    if (runner.Compile(#SourceScript))
    {
        // Gets the return values and output
        returns = runner.Run(scriptArgs);
        result  = returns.get_Item(0);
        info(int2str(result));
    }
    else
    {
        errors = runner.get_CompileErros();
        enumerator = errors.GetEnumerator();
        while (enumerator.MoveNext())
        {
            error(enumerator.get_Current());
        }
    }

}

 

执行完上面这段job程序,Dynamics AX就会编译#SourceScript宏中定义好的C#脚本,然后执行脚本程序,并弹出了一个infolog,上面显示了计算结果:30。如下:

这是如何实现的?

如果你直接将上面的job导入到AOT里去执行,那么在编译阶段就会出错,原因是你的Dynamics AX根本无法找到SunnyChen.CSharpScript.ScriptRunner这一类型。这个类型是我自己编写的一个.NET的类,它使用了.NET提供的CodeDom(Code Document Object Model)和Reflection的机制实现C#脚本的编译和执行。我使用Visual Studio创建了一个名为SunnyChen.CSharpScript的Class Library项目,然后往该项目中添加了这个类:

public class ScriptRunner
{
    private Assembly assembly;
    private StringCollection compileErrors = new StringCollection();

    public bool Compile(string script)
    {
        try
        {
            CSharpCodeProvider codeProvider = new CSharpCodeProvider();
            CompilerParameters compilerParameters = new CompilerParameters();
            compilerParameters.GenerateExecutable = false;
            compilerParameters.GenerateInMemory = true;
            compilerParameters.IncludeDebugInformation = false;
            compilerParameters.ReferencedAssemblies.Add("System.dll");
            compilerParameters.ReferencedAssemblies.Add("System.Windows.Forms.dll");
            compilerParameters.ReferencedAssemblies.Add("System.XML.dll");
            CompilerResults results = 
                codeProvider.CompileAssemblyFromSource(compilerParameters, script);

            if (!results.Errors.HasErrors)
            {
                this.assembly = results.CompiledAssembly;
                return true;
            }
            else
            {
                this.compileErrors.Clear();
                foreach (CompilerError ce in results.Errors)
                {
                    compileErrors.Add(ce.ErrorText);
                }
                return false;
            }
        }
        catch (Exception e)
        {
            EventLog.WriteEntry("CSharpScript", 
                e.Message, EventLogEntryType.Error); 
            throw;
        }
    }

    public StringCollection CompileErros
    {
        get
        {
            return compileErrors;
        }
    }

    public ArrayList Run(ArrayList args)
    {
        ArrayList ret = new ArrayList();
        try
        {
            Type entryType = null;
            MethodInfo entryPoint = null;
            foreach (var type in assembly.GetExportedTypes())
            {
                if (type.Name.Equals("Script"))
                {
                    entryType = type;
                    break;
                }
            }
            foreach (var methodInfo in entryType.GetMethods(BindingFlags.Public | 
                BindingFlags.Static))
            {
                if (methodInfo.Name.Equals("EntryMethod"))
                {
                    entryPoint = methodInfo;
                    break;
                }
            }
            Type returnType = entryPoint.ReturnType;
            ParameterInfo[] parameters = entryPoint.GetParameters();
            if (returnType.Equals(typeof(ArrayList)) &&
                parameters != null &&
                parameters.Count() == 1 &&
                parameters[0].ParameterType.Equals(typeof(ArrayList)))
            {
                ret = (ArrayList)entryPoint.Invoke(null, new object[] { args });
            }

        }
        catch(Exception e)
        {
            StringBuilder sb = new StringBuilder();
            
            sb.Append(e.Message);
            sb.Append("-->");
            sb.Append(e.StackTrace);
            sb.Append(Environment.NewLine);
            Exception inner = e.InnerException;
            while (inner != null)
            {
                sb.Append(inner.Message);
                sb.Append("-->");
                sb.Append(inner.StackTrace);
                sb.Append(Environment.NewLine);
                inner = inner.InnerException;
            }
            EventLog.WriteEntry("CSharpScript", sb.ToString(), EventLogEntryType.Error);
            throw;
        }

        return ret;
    }
}

 

在编译选项中,需要对SunnyChen.CSharpScript项目进行数字签名:

签名完,编译好以后,就将编译出来的SunnyChen.CSharpScript.dll安装到GAC里。方法是,在Run里输入c:\windows\assembly,然后将这个dll拖拽到打开的窗口中即可。

最后一步,将SunnyChen.CSharScript添加到Dynamics AX的AOT\References中,再回过头来编译上面的job。此时job能够顺利通过编译并正确执行。

这样做有什么实际应用?

看上去这只是一个噱头,就是一种技术把戏,貌似在实际应用中没什么特别的用处。其实,这个技术可以帮我们解决这样的场景:比如产品销售时,优惠策略的动态配置。针对不同的产品、分类或者地域,产品价格优惠的计算是非常复杂的,通常情况下我们也只能在系统设计的时候引入常见的几种促销优惠方式,或者更灵活一点,提供接口,便于今后二次开发进行扩展。然而这两种方式都是需要软件开发人员介入的,而不是面向最终用户的。当用户打算修改优惠策略时,就不得不去做系统扩展,甚至改动现有的逻辑。

我做了一个原型,就是根据CustTable中所有客户的成交交易总额进行折扣计算。比如当用户交易额超过2000元,则给8.7折优惠,如果没有超过,则给9.5折优惠,从而在Base Amount的基础上,计算出折扣金额和最后应付款额。请看:

在上面的Form中,我定义了折扣计算的代码,这个代码可以包含基于result(折扣额)和totalAmount(总交易额)计算的C#代码。代码可以包含任何C#支持的语法。当然,最终用户对编程肯定是一窍不通的,不过在这里输入几个if...else或者switch...case,应该还是问题不大的。下图就是运行时的结果:

源代码

本文提供SunnyChen.CSharpScript的源码下载。执行C#的job我就不提供了,读者自己从上文中拷贝即可。

点击下载此文件

posted @ 2010-01-20 08:37  dax.net  阅读(976)  评论(3编辑  收藏  举报