I am using Roslyn to run C# code from text.
It works, but I can't figure out how can I use my project class methods, didn't find a way to reference my project methods. I can't use a dll as there too many classes and forms I need to use.
For exmaple, using the Number set method, or the DoMultiAction() method inside the roslyn ExecuteUserCodeTest() text code. How to do that they will be referenced?
Is it even possible? Any explanation or example will be appreciated.
My Code:
namespace bot1
{
class Testing
{
private int number;
public int Number
{
get { return number; }
set { this.number = value; }
}
public int DoMultiAction(int num1, int num2)
{
return num1 * num2 * Number;
}
public void ExecuteUserCodeTest()
{
String text = #"
using System;
using System.Drawing;
using System.Windows.Forms;
namespace RoslynCompileSample
{
public class Writer
{
public void Write(String text)
{
MessageBox.Show(text);
}
}
}";
// define source code, then parse it (to the type used for compilation)
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(text);
// define other necessary objects for compilation
string assemblyName = Path.GetRandomFileName();
MetadataReference[] references = new MetadataReference[]
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location),
MetadataReference.CreateFromFile(typeof(System.Drawing.Point).Assembly.Location),
MetadataReference.CreateFromFile(typeof(System.Windows.Forms.MessageBox).Assembly.Location),
MetadataReference.CreateFromFile(typeof (ScriptManagerHandler.ScriptHandler).Assembly.Location)
};
// analyse and generate IL code from syntax tree
CSharpCompilation compilation = CSharpCompilation.Create(
assemblyName,
syntaxTrees: new[] { syntaxTree },
references: references,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
using (var ms = new MemoryStream())
{
// write IL code into memory
EmitResult result = compilation.Emit(ms);
if (!result.Success)
{
// handle exceptions
IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic =>
diagnostic.IsWarningAsError ||
diagnostic.Severity == DiagnosticSeverity.Error);
String error = "";
foreach (Diagnostic diagnostic in failures)
{
error += "" + diagnostic.Id + ", " + diagnostic.GetMessage() + "\n";
}
if (error != "")
MessageBox.Show(error);
}
else
{
// load this 'virtual' DLL so that we can use
ms.Seek(0, SeekOrigin.Begin);
Assembly assembly = Assembly.Load(ms.ToArray());
// create instance of the desired class and call the desired function
Type type = assembly.GetType("RoslynCompileSample.Writer");
object obj = Activator.CreateInstance(type);
type.InvokeMember("Write",
BindingFlags.Default | BindingFlags.InvokeMethod,
null,
obj,
new object[] { "Hello World" });
}
}
}
}
}
Related
I'm getting a thrown exception when attempting to import a SqlDataRecord with a DateTime2 data type. There is no exception thrown in this snippet, but the cause of the later exception is evident in the following code.
using System;
using Microsoft.SqlServer.Server;
using System.Data.SqlClient;
using System.Data;
using System.Web.UI.WebControls;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Diagnostics;
namespace Testing123
{
public class Program
{
public static object SQLTParser(SqlMetaData smd, string val)
{
TypeCode systc = Parameter.ConvertDbTypeToTypeCode(smd.DbType);
try
{
return Convert.ChangeType(val, systc);
}
catch (Exception ex)
{
if (ex is InvalidCastException || ex is FormatException || ex is OverflowException)
{
Console.WriteLine("Exception reached casting " + val + " to " + Type.GetType("System." + Enum.GetName(typeof(TypeCode), systc)) + ": " + ex.Message + ex.ToString()); //smd.GetType().Name
return null;
}
else
{
Console.WriteLine("Null value exception");
return null;
}
}
}
public static void Main()
{
SqlMetaData sqmd = new SqlMetaData("dt", SqlDbType.DateTime2, 27, 7);
SqlDataRecord sdr = new SqlDataRecord(sqmd);
sdr.SetValue(0, SQLTParser(sqmd, "2017-01-12 01:23:12.3456789"));
//set BreakPoint
//sdr -> Non-Public members -> _columnMetaData[0] -> Precision = 27
//sdr -> Non-Public members -> _columnSmiMetaData[0] -> Non-Public members -> Precision = 0
}
}
}
As noted, if you set the breakpoint and watches as indicated in MS Visual Studio*, Precision does not match between columnMetaData and columnSmiMetaData, which by the second such entry (!!) throws an exception:
Metadata for field 'dt' of record '2' did not match the original record's metadata.
which matches the exception thrown by
line 3755 of ValueUtilsSmi
due to the return of MetadataUtilsSmi.IsCompatible
Essentially, precision in the field's metadata of record 2 doesn't match that which is in the SmiMetaData of record 1. In record 1, the MD and SMD don't match either, but based on the logic Microsoft was using for an IEnumerator'd SqlDataRecord, it doesn't become an issue until the 2nd record.
Is this a MS bug? Or is there a way to force the precision value of the SmiMetaData? Or ignore that particular field check in ValueUtilsSmi? Specifying SqlMetaData sqmd = new SqlMetaData("dt", SqlDbType.DateTime2, 0, 7); allows the parsing to proceed, but drops sub-second precision from the data.
Here is, in a nutshell, how I'm attempting to send the data to a database. Apologies if this portion is not a complete example.
public class FullStreamingDataRecord : IEnumerable<SqlDataRecord>
{
private string _filePath;
public bool _hasHeader { get; private set; }
private ParserDict _pd; //notably has colStructure which is an array of SqlMetaData[]
public FullStreamingDataRecord(string FilePath, ParserDict pd)
{
_filePath = FilePath;
_pd = pd;
_hasHeader = true;
}
public static object SQLTParser(SqlMetaData smd, string val)
{
TypeCode systc = Parameter.ConvertDbTypeToTypeCode(smd.DbType);
try
{
return Convert.ChangeType(val, systc);
}
catch (Exception ex)
{
if (ex is InvalidCastException || ex is FormatException || ex is OverflowException)
{
Console.WriteLine("Exception reached casting " + val + " to " + Type.GetType("System."+Enum.GetName(typeof(TypeCode),systc)) + ": " + ex.Message + ex.ToString()); //smd.GetType().Name
return null; //smd.TParse(val);
}
else
{
Console.WriteLine("Null value exception casting...attempting a different method");
return null; smd.TParse(val);
}
}
}
public IEnumerator<SqlDataRecord> GetEnumerator()
{
int len = this._pd.colStructure.Length;
StreamReader fileReader = null;
try
{
using (fileReader = new StreamReader(this._filePath))
{
string inputRow = "";
string[] inputColumns = new string[len];
if (_hasHeader && !fileReader.EndOfStream)
{
inputRow = fileReader.ReadLine(); //and ignore
}
while (!fileReader.EndOfStream)
{
string temp = "";
inputRow = fileReader.ReadLine();
inputColumns = inputRow.Split(new char[]{','},len);
SqlDataRecord dataRecord = this._pd.colStructure
for (int j = 0; j < len; j++) { // i = counter for input columns
string currentKey = this._pd.colStructure[j].SqlMetaData.Name;
string curval = inputColumns[j];
var ty = this._pd.colStructure[j].SqlMetaData; //.DbType;
dataRecord.SetValue(j, SQLTParser(ty, curval));
// dataRecord.GetSqlMetaData(j).Adjust(dataRecord.GetValue(j));
}
yield return dataRecord;
}
}
}
// no catch block allowed due to the "yield" command
finally
{
fileReader.Close();
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
Which I call with this in my Main()
using (SqlConnection conn = new DBConnect().conn)
{
conn.Open();
SqlCommand importProc = new SqlCommand("tvp_"+pd.tableDestination, conn);
importProc.CommandType = CommandType.StoredProcedure;
importProc.CommandTimeout = 300;
SqlParameter importTable = new SqlParameter();
importTable.ParameterName = "#ImportTable";
importTable.TypeName = "dbo.tt_"+pd.tableDestination;
importTable.SqlDbType = SqlDbType.Structured;
importTable.Value = new FullStreamingDataRecord(fn, pd);
importProc.Parameters.Add(importTable);
importProc.ExecuteNonQuery(); //this line throws the exception
}
Apologies for not Console.WriteLineing the values of these watches.
Unfortunately, it seems that these parameters are protected against examination using Reflection's sdr.GetType().GetProperties(), even with the appropriate BindingFlags set. But at least you can see the values in debug mode!
I have had success using this tutorial: http://www.codeproject.com/Tips/715891/Compiling-Csharp-Code-at-Runtime to set up a framework for runtime compilation and execution of C# code. Below is the code I currently have:
public static class CodeCompiler {
public static object InterpretString(string executable) {
string compilation_string =
#"
static class RuntimeCompilationCode {
public static void Main() {}
public static object Custom() {
/* CODE HERE */
}
}";
compilation_string = compilation_string.Replace("/* CODE HERE */", executable);
CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerParameters compiler_parameters = new CompilerParameters();
// True - memory generation, false - external file generation
compiler_parameters.GenerateInMemory = true;
// True - exe file generation, false - dll file generation
compiler_parameters.GenerateExecutable = true;
// Compile
CompilerResults results = provider.CompileAssemblyFromSource(compiler_parameters, compilation_string);
// Check errors
if (results.Errors.HasErrors) {
StringBuilder builder = new StringBuilder();
foreach (CompilerError error in results.Errors) {
builder.AppendLine(String.Format("Error ({0}): {1}", error.ErrorNumber, error.ErrorText));
}
throw new InvalidOperationException(builder.ToString());
}
// Execute
Assembly assembly = results.CompiledAssembly;
Type program = assembly.GetType("RuntimeCompilationCode");
MethodInfo execute = program.GetMethod("Custom");
return execute.Invoke(null, null);
}
}
I can pass a statement in the form of a string (ex. "return 2;") to InterpretString() and it will be compiled and executed as part of the Custom() function. However I am wondering if it is possible to use the same approach to execute a method that is in my original file. For instance, suppose the CodeCompiler class had another method returnsTwo() which returns the integer 2. Is there a way to call such a method by passing "CodeCompiler.returnsTwo();" or a similar string to InterpretString()?
Provided that the function is a static function this should not be a problem, as long as you add the appropriate reference to the compilation. I've done this short of thing on several projects.
If the CodeCompiler is in your current executable you have to include the references in this fashion:
string exePath = Assembly.GetExecutingAssembly().Location;
string exeDir = Path.GetDirectoryName(exePath);
AssemblyName[] assemRefs = Assembly.GetExecutingAssembly().GetReferencedAssemblies();
List<string> references = new List<string>();
foreach (AssemblyName assemblyName in assemRefs)
references.Add(assemblyName.Name + ".dll");
for (int i = 0; i < references.Count; i++)
{
string localName = Path.Combine(exeDir, references[i]);
if (File.Exists(localName))
references[i] = localName;
}
references.Add(exePath);
CompilerParameters compiler_parameters = new CompilerParameters(references.ToArray())
Currently I'm working on a custom importer for Ironpython, which should add an abstraction layer for writing custom importer. The abstraction layer is an IronPython module, which bases on PEP 302 and the IronPython zipimporter module. The architecture looks like this:
For testing my importer code, I've written a simple test package with modules, which looks like this:
/Math/
__init__.py
/MathImpl/
__init__.py
__Math2__.py
/Math/__init__.py:
print ('Import: /Math/__init__.py')
/Math/MathImpl/__init__.py:
# Sample math package
print ('Begin import /Math/MathImpl/__init__.py')
import Math2
print ('End import /Math/MathImpl/__init__.py: ' + str(Math2.add(1, 2)))
/Math/MathImpl/Math2.py:
# Add two values
def add(x, y):
return x + y
print ('Import Math2.py!')
If i try to import MathImpl like this in a script: import Math.MathImpl
My genericimporter get's called and searchs for some module/package in the find_module method. Which returns an instance of the importer if found, else not:
public object find_module(CodeContext/*!*/ context, string fullname, params object[] args)
{
// Set module
if (fullname.Contains("<module>"))
{
throw new Exception("Why, why does fullname contains <module>?");
}
// Find resolver
foreach (var resolver in Host.Resolver)
{
var res = resolver.GetModuleInformation(fullname);
// If this script could be resolved by some resolver
if (res != ResolvedType.None)
{
this.resolver = resolver;
return this;
}
}
return null;
}
If find_module is called the first time,fullname contains Math, which is ok, because Math should be imported first. The second time find_module is called, Math.MathImpl should be imported, the problem here is, that fullname has now the value <module>.MathImpl, instead of Math.MathImpl.
My idea was, that the module name (__name__) is not set correctly when Math was imported, but i set this in any case when importing the module in load_module:
public object load_module(CodeContext/*!*/ context, string fullname)
{
string code = null;
GenericModuleCodeType moduleType;
bool ispackage = false;
string modpath = null;
PythonModule mod;
PythonDictionary dict = null;
// Go through available import types by search-order
foreach (var order in _search_order)
{
string tempCode = this.resolver.GetScriptSource(fullname + order.Key);
if (tempCode != null)
{
moduleType = order.Value;
code = tempCode;
modpath = fullname + order.Key;
Console.WriteLine(" IMPORT: " + modpath);
if ((order.Value & GenericModuleCodeType.Package) == GenericModuleCodeType.Package)
{
ispackage = true;
}
break;
}
}
// of no code was loaded
if (code == null)
{
return null;
}
var scriptCode = context.ModuleContext.Context.CompileSourceCode
(
new SourceUnit(context.LanguageContext, new SourceStringContentProvider(code), modpath, SourceCodeKind.AutoDetect),
new IronPython.Compiler.PythonCompilerOptions() { },
ErrorSink.Default
);
// initialize module
mod = context.ModuleContext.Context.InitializeModule(modpath, context.ModuleContext, scriptCode, ModuleOptions.None);
dict = mod.Get__dict__();
// Set values before execute script
dict.Add("__name__", fullname);
dict.Add("__loader__", this);
dict.Add("__package__", null);
if (ispackage)
{
// Add path
string subname = GetSubName(fullname);
string fullpath = string.Format(fullname.Replace(".", "/"));
List pkgpath = PythonOps.MakeList(fullpath);
dict.Add("__path__", pkgpath);
}
else
{
StringBuilder packageName = new StringBuilder();
string[] packageParts = fullname.Split(new char[] { '/' });
for (int i = 0; i < packageParts.Length - 1; i++)
{
if (i > 0)
{
packageName.Append(".");
}
packageName.Append(packageParts[i]);
}
dict["__package__"] = packageName.ToString();
}
var scope = context.ModuleContext.GlobalScope;
scriptCode.Run(scope);
return mod;
}
I hope some one has an idea, why this happens. A few line which also may cause the problem are:
var scriptCode = context.ModuleContext.Context.CompileSourceCode
(
new SourceUnit(context.LanguageContext, new SourceStringContentProvider(code), modpath, SourceCodeKind.AutoDetect),
new IronPython.Compiler.PythonCompilerOptions() { },
ErrorSink.Default
);
and
mod = context.ModuleContext.Context.InitializeModule(modpath, context.ModuleContext, scriptCode, ModuleOptions.None);
Because i don't know, whether creating a module this way is completly correct.
The problem can be reproduced downloading this project/branch: https://github.com/simplicbe/Simplic.Dlr/tree/f_res_noid and starting Sample.ImportResolver. An exception in find_module will be raised.
Thank you all!
This problem is solved. Modpath what not allowed to contains /. In general only chars were allowed, which also can be in a file-name.
Maybe this is helpful for someone else...
I read that you can't compile C# 6.0 with CSharpCodeProvider and therefor trying to do with with Roslyn. But I can't find a good example how to load a file and then compile it to a dll.
How should I write something similar to this code with Roslyn? Or is there some other way to do it? Now when I try to compile files that contain reference to projects with C# 6.0 code it just say "The type or namespace name 'x' does not exist in the namespace 'y' (are you missing an assembly reference?)"
public string CompileCode()
{
var provider = new CSharpCodeProvider();
var outputPath = Path.Combine(Path.GetDirectoryName(_path), $"Code.dll");
var compilerparams = new CompilerParameters(_referencedAssemblies, outputPath);
CompilerResults results = provider.CompileAssemblyFromFile(compilerparams, _path);
var dllPath = results.PathToAssembly;
if (!results.Errors.HasErrors)
return dllPath;
PrintError(results.Errors);
return "";
}
In summary I want to:
Load a C# file
Compile it to a dll so I can load it later.
I have created a sample for you to work with. You need to tweak it to use the run time for .Net 4.6 so that CSharp6 version is availble to you. I have added little details so that you can choose the options of compilations.
Changes required -
Change the path of runtime to target .Net 4.6
Change the LanguageVersion.Csharp5 to LanguageVersion.Csharp6 in below sample.
class Program
{
private static readonly IEnumerable<string> DefaultNamespaces =
new[]
{
"System",
"System.IO",
"System.Net",
"System.Linq",
"System.Text",
"System.Text.RegularExpressions",
"System.Collections.Generic"
};
private static string runtimePath = #"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5.1\{0}.dll";
private static readonly IEnumerable<MetadataReference> DefaultReferences =
new[]
{
MetadataReference.CreateFromFile(string.Format(runtimePath, "mscorlib")),
MetadataReference.CreateFromFile(string.Format(runtimePath, "System")),
MetadataReference.CreateFromFile(string.Format(runtimePath, "System.Core"))
};
private static readonly CSharpCompilationOptions DefaultCompilationOptions =
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
.WithOverflowChecks(true).WithOptimizationLevel(OptimizationLevel.Release)
.WithUsings(DefaultNamespaces);
public static SyntaxTree Parse(string text, string filename = "", CSharpParseOptions options = null)
{
var stringText = SourceText.From(text, Encoding.UTF8);
return SyntaxFactory.ParseSyntaxTree(stringText, options, filename);
}
static void Main(string[] args)
{
var fileToCompile = #"C:\Users\DesktopHome\Documents\Visual Studio 2013\Projects\ConsoleForEverything\SignalR_Everything\Program.cs";
var source = File.ReadAllText(fileToCompile);
var parsedSyntaxTree = Parse(source, "", CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp5));
var compilation
= CSharpCompilation.Create("Test.dll", new SyntaxTree[] { parsedSyntaxTree }, DefaultReferences, DefaultCompilationOptions);
try
{
var result = compilation.Emit(#"c:\temp\Test.dll");
Console.WriteLine(result.Success ? "Sucess!!" : "Failed");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
Console.Read();
}
This would need little tweaks but it should give you desired results. Change it as you may wish.
You have to use the NuGet package Microsoft.CodeAnalysis.CSharp.
var syntaxTree = CSharpSyntaxTree.ParseText(source);
CSharpCompilation compilation = CSharpCompilation.Create(
"assemblyName",
new[] { syntaxTree },
new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) },
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
using (var dllStream = new MemoryStream())
using (var pdbStream = new MemoryStream())
{
var emitResult = compilation.Emit(dllStream, pdbStream);
if (!emitResult.Success)
{
// emitResult.Diagnostics
}
}
I'm using Roslyn to try and compile and run code at runtime. I've ysed some code I found online and have it somewhat working.
public Type EvalTableScript(string Script, CRMMobileFramework.EnbuUtils EnbuUtils, CRMMobileFramework.Includes.DBAdapter dbConn)
{
var syntaxTree = SyntaxTree.ParseText(Script);
var compilation = Compilation.Create("EnbuScript.dll",
options: new CompilationOptions(outputKind: OutputKind.DynamicallyLinkedLibrary),
references: new[]
{
new MetadataFileReference(typeof(object).Assembly.Location),
new MetadataFileReference(typeof(EnbuUtils).Assembly.Location),
new MetadataFileReference(typeof(DBAdapter).Assembly.Location),
MetadataFileReference.CreateAssemblyReference("System.Data"),
MetadataFileReference.CreateAssemblyReference("System.Linq"),
MetadataFileReference.CreateAssemblyReference("System"),
MetadataFileReference.CreateAssemblyReference("System.XML")
},
syntaxTrees: new[] { syntaxTree });
var diagnostics = compilation.GetDiagnostics();
foreach (var diagnostic in diagnostics)
{
Console.WriteLine("Error: {0}", diagnostic.Info.GetMessage());
}
Assembly assembly;
using (var stream = new MemoryStream())
{
EmitResult emitResult = compilation.Emit(stream);
assembly = Assembly.Load(stream.GetBuffer());
}
Type ScriptClass = assembly.GetType("EnbuScript");
// Pass back the entire class so we can call it at the appropriate time.
return ScriptClass;
}
Then I'm trying to call this:
string Script = #"
using System;
using System.Data;
using System.IO;
using System.Linq;
public class EnbuScript
{
public string PostInsertRecord(CRMMobileFramework.EnbuUtils EnbuUtils,CRMMobileFramework.Includes.DBAdapter dbConn)
{
string ScriptTable = ""QuoteItems"";
DataSet EntityRecord = dbConn.FindRecord(""*"", ScriptTable, ""QuIt_LineItemID='"" + EnbuUtils.GetContextInfo(ScriptTable) + ""'"", """", 1, 1, false);
string OrderId = EntityRecord.Tables[""item""].Rows[0][""QuIt_orderquoteid""].ToString();
string UpdateOrderTotalCommand = ""UPDATE Quotes SET Quot_nettamt = (select SUM(QuIt_listprice * quit_quantity) from QuoteItems where quit_orderquoteid = "" + OrderId + "" ) where Quot_OrderQuoteID = "" + OrderId;
dbConn.ExecSql(UpdateOrderTotalCommand);
return ""Complete"";
}
}";
Type EnbuScript = EnbuUtils.EvalTableScript(Script, EnbuUtils, dbConn);
MethodInfo methodInfo = EnbuScript.GetMethod("InsertRecord");
object[] parameters = { EnbuUtils, dbConn };
string InsertRecordResult = methodInfo.Invoke(null, parameters).ToString();
As you can see I've been messing around with trying to pass parameters to the compilation.
Basically I've got 4 functions I need to support, that will come in as a string. What I'm trying to do is create a class for these 4 functions and compile and run them. This part works.
What I now need to be able to do is pass class instances to this. In the code you'll see a dbConn which is basically my database connection. I need to pass the instance of this to the method I'm calling at runtime so it has it's correct context.
I have another implementation of this where I'm using the Roslyn session. I originally tried to use this and override my function at runtime but that didn't work either. See below what I tried:
public static void EvalTableScript(ref EnbuUtils EnbuUtils, DBAdapter dbConn, string EvaluateString)
{
ScriptEngine roslynEngine = new ScriptEngine();
Roslyn.Scripting.Session Session = roslynEngine.CreateSession(EnbuUtils);
Session.AddReference(EnbuUtils.GetType().Assembly);
Session.AddReference(dbConn.GetType().Assembly);
Session.AddReference("System.Web");
Session.AddReference("System.Data");
Session.AddReference("System");
Session.AddReference("System.XML");
Session.ImportNamespace("System");
Session.ImportNamespace("System.Web");
Session.ImportNamespace("System.Data");
Session.ImportNamespace("CRMMobileFramework");
Session.ImportNamespace("CRMMobileFramework.Includes");
try
{
var result = (string)Session.Execute(EvaluateString);
}
catch (Exception ex)
{
}
}
I tried to call this using:
string PostInsertRecord = "" +
" public override void PostInsertRecord() " +
"{ " +
" string ScriptTable = \"QuoteItems\"; " +
"DataSet EntityRecord = dbConn.FindRecord(\"*\", ScriptTable, \"QuIt_LineItemID='\" + EnbuUtils.GetContextInfo(ScriptTable) + \"'\", \"\", 1, 1, false); " +
"string OrderId = EntityRecord.Tables[\"item\"].Rows[0][\"QuIt_orderquoteid\"].ToString(); " +
"string UpdateOrderTotalCommand = \"UPDATE Quotes SET Quot_nettamt = (select SUM(QuIt_listprice * quit_quantity) from QuoteItems where quit_orderquoteid = \" + OrderId + \" ) where Quot_OrderQuoteID = \" + OrderId; " +
"dbConn.ExecSql(UpdateOrderTotalCommand); " +
"} ";
The function is declared as a public virtual void in the EnbuUtils class but it says it doesn't have a suitable method to override.
Safe to say, I'm stumped!
Any help appreciated!
Thanks
I got this in the end - this first method was very close to what I actually needed. Changed the method to static and had to add a few references including the full namespace.