diff --git a/src/CSScriptLib/src/CSScriptLib/CSScriptLib.csproj b/src/CSScriptLib/src/CSScriptLib/CSScriptLib.csproj index bcadf1b2..97a2a328 100644 --- a/src/CSScriptLib/src/CSScriptLib/CSScriptLib.csproj +++ b/src/CSScriptLib/src/CSScriptLib/CSScriptLib.csproj @@ -1,129 +1,131 @@  - - netstandard2.0 - CSScriptLib - latest - CS-Script - - - false - false - false - true - true - true - snupkg - true - 4.14.7.0 - Oleg Shilo - CS-Script engine Class Library for .NET - (C) 2018-2026 Oleg Shilo - - https://github.com/oleg-shilo/cs-script - - https://github.com/oleg-shilo/cs-script.git - Git - C#, scripting, script, dynamic, .NET. .NET Core - --- + + netstandard2.0 + CSScriptLib + latest + CS-Script + + + false + false + false + true + true + true + snupkg + true + 4.14.8.0 + Oleg Shilo + CS-Script engine Class Library for .NET + (C) 2018-2026 Oleg Shilo + + https://github.com/oleg-shilo/cs-script + + https://github.com/oleg-shilo/cs-script.git + Git + C#, scripting, script, dynamic, .NET. .NET Core + + --- -## Changes + ## Changes -### CLI + ### CLI -- <no changes> + - <no changes> -### CSScriptLib + ### CSScriptLib -- #453: CSScriptLib: allow //css_precompiler directive proessing for Roslyn compiler engine. - true - 4.14.7.0 - 4.14.7.0 - 4.14.7.0 - MIT - css_logo.png - True - sgKey.snk - - - - - - CSScriptLib.xml - AnyCPU - TRACE;class_lib - - - CSScriptLib.xml - TRACE;class_lib - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - - - - - - - - - - Resources.resx - True - True - - - - - Resources.Designer.cs - ResXFileCodeGenerator - CSScripting - - - - - + - #453: CSScriptLib: allow //css_precompiler directive proessing for Roslyn compiler engine. + + true + 4.14.8.0 + 4.14.8.0 + 4.14.8.0 + MIT + css_logo.png + True + sgKey.snk + + + + + + CSScriptLib.xml + AnyCPU + TRACE;class_lib + + + CSScriptLib.xml + TRACE;class_lib + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + + + + + + + + + + Resources.resx + True + True + + + + + Resources.Designer.cs + ResXFileCodeGenerator + CSScripting + + + + + \ No newline at end of file diff --git a/src/CSScriptLib/src/CSScriptLib/Evaluator.CodeDom.cs b/src/CSScriptLib/src/CSScriptLib/Evaluator.CodeDom.cs index bbf89b0f..becc862a 100644 --- a/src/CSScriptLib/src/CSScriptLib/Evaluator.CodeDom.cs +++ b/src/CSScriptLib/src/CSScriptLib/Evaluator.CodeDom.cs @@ -38,6 +38,7 @@ using System.Reflection; using System.Text; using System.Threading; +using Microsoft.CodeAnalysis.Scripting; using csscript; using CSScripting; using CSScripting.CodeDom; @@ -167,6 +168,66 @@ protected override void Validate(CompileInfo info) "named parent class. You are using CodeDomEvaluator so you should not set CompileInfo.RootClass to any custom value"); } + /// + /// Resets Evaluator. + /// + /// Resetting means clearing all referenced assemblies, recreating evaluation infrastructure + /// (e.g. compiler setting) and reconnection to or recreation of the underlying compiling services. + /// + /// + /// Optionally the default current AppDomain assemblies can be referenced automatically with + /// . + /// + /// + /// + /// if set to true the default assemblies of the current AppDomain will be referenced + /// (see method). + /// + /// The freshly initialized instance of the . + public override IEvaluator Reset(bool referenceDomainAssemblies = true) + { + referencedAssemblies.Clear(); + referencedAssembliesAliases.Clear(); + + if (referenceDomainAssemblies) + ReferenceDomainAssemblies(); + + return this; + } + + /// + /// Clones itself as . + /// + /// This method returns a freshly initialized copy of the + /// . The cloning 'depth' can be + /// controlled by the . + /// + /// + /// This method is a convenient technique when multiple + /// instances are required (e.g. + /// for concurrent script evaluation). + /// + /// + /// if set to true all referenced + /// assemblies from the parent + /// will be referenced in the cloned copy. + /// The freshly initialized instance of the + /// . + public override IEvaluator Clone(bool copyRefAssemblies = true) + { + var clone = new CodeDomEvaluator(); + if (copyRefAssemblies) + { + clone.Reset(false); + foreach (var a in this.GetReferencedAssemblies()) + clone.ReferenceAssembly(a); + clone.referencedAssembliesAliases.AddItems(this.referencedAssembliesAliases); + } + + return clone; + } + /// /// Compiles the specified script text. /// @@ -468,7 +529,14 @@ public string CompileAssemblyFromCode(string scriptText, CompileInfo info, out P gac_asms.AddRange(Directory.GetFiles(gac, "Microsoft.*.dll").Where(x => !x.Contains("Native"))); foreach (string file in gac_asms.Concat(ref_assemblies).Distinct()) - refs_args.Add($"/r:\"{file}\""); + { + var aliasesList = ""; + + var aliases = AliasesOf(file); + if (aliases.Any()) + aliasesList = aliases.JoinBy(",") + "="; + refs_args.Add($"/r:{aliasesList}\"{file}\""); + } } else { @@ -626,6 +694,9 @@ public string CompileAssemblyFromCode(string scriptText, CompileInfo info, out P } List referencedAssemblies = new List(); + Dictionary referencedAssembliesAliases = new(); + + string[] AliasesOf(string assembly) => referencedAssembliesAliases.TryGetValue(assembly, out var aliases) ? aliases : []; /// /// References the given assembly. @@ -635,6 +706,7 @@ public string CompileAssemblyFromCode(string scriptText, CompileInfo info, out P /// /// /// The assembly instance. + /// The optional aliases for the assembly. /// /// The instance of the to allow fluent interface. /// @@ -642,7 +714,7 @@ public string CompileAssemblyFromCode(string scriptText, CompileInfo info, out P /// Current version of {EngineName} doesn't support referencing assemblies " + "which are /// not loaded from the file location. /// - public override IEvaluator ReferenceAssembly(Assembly assembly) + public override IEvaluator ReferenceAssembly(Assembly assembly, string[] aliases = null) { if (assembly != null)//this check is needed when trying to load partial name assemblies that result in null { @@ -655,6 +727,9 @@ public override IEvaluator ReferenceAssembly(Assembly assembly) if (referencedAssemblies.FirstOrDefault(x => asmFile.SamePathAs(x)) == null) referencedAssemblies.Add(asmFile); + + if (aliases != null) + referencedAssembliesAliases[asmFile] = aliases; } return this; } diff --git a/src/CSScriptLib/src/CSScriptLib/Evaluator.Roslyn.cs b/src/CSScriptLib/src/CSScriptLib/Evaluator.Roslyn.cs index 8ec882ce..8e35df2f 100644 --- a/src/CSScriptLib/src/CSScriptLib/Evaluator.Roslyn.cs +++ b/src/CSScriptLib/src/CSScriptLib/Evaluator.Roslyn.cs @@ -384,9 +384,19 @@ SyntaxTree createTree(string code, string path) => foreach (var asm in allRefs) { - var metadata = ToMetadata(asm); + AssemblyMetadata metadata = ToMetadata(asm); + if (metadata != null) - references.Add(metadata.GetReference()); + { + var mdRef = metadata.GetReference(); + + var aliases = AliasesOf(asm); + + if (aliases.Any() && mdRef is PortableExecutableReference peRef) + mdRef = peRef.WithProperties(peRef.Properties.WithAliases(aliases)); + + references.Add(mdRef); + } } // switch (effectiveCodeKind) @@ -591,6 +601,7 @@ SyntaxTree createTree(string code, string path) => /// /// /// The assembly instance. + /// The optional aliases for the assembly. /// /// The instance of the to allow fluent interface. /// @@ -598,7 +609,7 @@ SyntaxTree createTree(string code, string path) => /// Current version of {EngineName} doesn't support referencing assemblies " + "which are /// not loaded from the file location. /// - public override IEvaluator ReferenceAssembly(Assembly assembly) + public override IEvaluator ReferenceAssembly(Assembly assembly, string[] aliases = null) { //Microsoft.Net.Compilers.1.2.0 - beta if (assembly.Location.IsEmpty() && !Runtime.IsSingleFileApplication) @@ -607,7 +618,11 @@ public override IEvaluator ReferenceAssembly(Assembly assembly) "which are not loaded from the file location."); if (!refAssemblies.Contains(assembly)) + { refAssemblies.Add(assembly); + if (aliases != null) + refAssembliesAliases[assembly] = aliases; + } return this; } @@ -711,6 +726,9 @@ public T Eval(string scriptText, CompileInfo info) } List refAssemblies = new List(); + Dictionary refAssembliesAliases = new Dictionary(); + + string[] AliasesOf(Assembly assembly) => refAssembliesAliases.TryGetValue(assembly, out var aliases) ? aliases : []; IEvaluator PrepareRefAssemblies() { @@ -731,11 +749,18 @@ IEvaluator PrepareRefAssemblies() { if (!CompilerSettings.MetadataReferences.OfType().Any(r => r.FilePath.SamePathAs(assembly.Location))) { - // Future assembly aliases support: - // MetadataReference.CreateFromFile("asm.dll", new - // MetadataReferenceProperties().WithAliases(new[] { "lib_a", - // "external_lib_a" } }) - CompilerSettings = CompilerSettings.AddReferences(assembly); + var aliases = AliasesOf(assembly); + + if (aliases.Any()) + { + var mdRef = MetadataReference.CreateFromFile( + assembly.Location(), + new MetadataReferenceProperties().WithAliases(aliases)); + + CompilerSettings = CompilerSettings.AddReferences(mdRef); + } + else + CompilerSettings = CompilerSettings.AddReferences(assembly); } } } @@ -769,6 +794,7 @@ IEvaluator PrepareRefAssemblies() public override IEvaluator Reset(bool referenceDomainAssemblies = true) { refAssemblies.Clear(); + refAssembliesAliases.Clear(); CompilerSettings = ScriptOptions.Default; if (referenceDomainAssemblies) @@ -776,6 +802,38 @@ public override IEvaluator Reset(bool referenceDomainAssemblies = true) return this; } + + /// + /// Clones itself as . + /// + /// This method returns a freshly initialized copy of the + /// . The cloning 'depth' can be + /// controlled by the . + /// + /// + /// This method is a convenient technique when multiple + /// instances are required (e.g. + /// for concurrent script evaluation). + /// + /// + /// if set to true all referenced + /// assemblies from the parent + /// will be referenced in the cloned copy. + /// The freshly initialized instance of the + /// . + public override IEvaluator Clone(bool copyRefAssemblies = true) + { + var clone = new RoslynEvaluator(); + if (copyRefAssemblies) + { + clone.Reset(false); + foreach (var a in this.GetReferencedAssemblies()) + clone.ReferenceAssembly(a); + + clone.refAssembliesAliases.AddItems(this.refAssembliesAliases); + } + return clone; + } } static class localExtensions diff --git a/src/CSScriptLib/src/CSScriptLib/EvaluatorBase.cs b/src/CSScriptLib/src/CSScriptLib/EvaluatorBase.cs index 2e614acc..143852e7 100644 --- a/src/CSScriptLib/src/CSScriptLib/EvaluatorBase.cs +++ b/src/CSScriptLib/src/CSScriptLib/EvaluatorBase.cs @@ -62,33 +62,14 @@ namespace CSScriptLib public class EvaluatorBase : IEvaluator where T : IEvaluator, new() { /// - /// Clones itself as . - /// - /// This method returns a freshly initialized copy of the - /// . The cloning 'depth' can be - /// controlled by the . - /// - /// - /// This method is a convenient technique when multiple - /// instances are required (e.g. - /// for concurrent script evaluation). - /// + /// Clones the current instance of the evaluator. /// - /// if set to true all referenced - /// assemblies from the parent - /// will be referenced in the cloned copy. - /// The freshly initialized instance of the - /// . - public IEvaluator Clone(bool copyRefAssemblies = true) + /// + /// + /// + public virtual IEvaluator Clone(bool copyRefAssemblies = true) { - var clone = new T(); - if (copyRefAssemblies) - { - clone.Reset(false); - foreach (var a in this.GetReferencedAssemblies()) - clone.ReferenceAssembly(a); - } - return clone; + throw new NotImplementedException(); } static Assembly mscorelib = 333.GetType().Assembly; @@ -1280,9 +1261,10 @@ private bool ApplyPrecompilation(string script, PrecompilationContext retval, Ha /// /// /// The path to the assembly file. + /// The optional aliases for the assembly. /// The instance of the to /// allow fluent interface. - public IEvaluator ReferenceAssembly(string assembly) + public IEvaluator ReferenceAssembly(string assembly, string[] aliases = null) { var globalProbingDirs = CSScript.GlobalSettings.SearchDirs.ToList(); @@ -1291,7 +1273,7 @@ public IEvaluator ReferenceAssembly(string assembly) string asmFile = AssemblyResolver.FindAssembly(assembly, dirs).FirstOrDefault() ?? throw new Exception("Cannot find referenced assembly '" + assembly + "'"); - ReferenceAssembly(Assembly.LoadFile(asmFile)); + ReferenceAssembly(Assembly.LoadFile(asmFile), aliases); return this; } @@ -1309,9 +1291,10 @@ public IEvaluator ReferenceAssembly(string assembly) /// /// /// The assembly instance. + /// The optional aliases for the assembly. /// The instance of the to /// allow fluent interface. - public virtual IEvaluator ReferenceAssembly(Assembly assembly) + public virtual IEvaluator ReferenceAssembly(Assembly assembly, string[] aliases = null) => throw new NotImplementedException(); /// diff --git a/src/CSScriptLib/src/CSScriptLib/IEvaluator.cs b/src/CSScriptLib/src/CSScriptLib/IEvaluator.cs index 8fb72b59..c59844d3 100644 --- a/src/CSScriptLib/src/CSScriptLib/IEvaluator.cs +++ b/src/CSScriptLib/src/CSScriptLib/IEvaluator.cs @@ -875,10 +875,11 @@ public interface IEvaluator /// /// /// The path to the assembly file. + /// The optional aliases for the assembly. /// /// The instance of the to allow fluent interface. /// - IEvaluator ReferenceAssembly(string assembly); + IEvaluator ReferenceAssembly(string assembly, string[] aliases = null); /// /// References the given assembly. @@ -888,10 +889,11 @@ public interface IEvaluator /// /// /// The assembly instance. + /// The optional aliases for the assembly. /// /// The instance of the to allow fluent interface. /// - IEvaluator ReferenceAssembly(Assembly assembly); + IEvaluator ReferenceAssembly(Assembly assembly, string[] aliases = null); /// /// References the name of the assembly by its partial name. diff --git a/src/Tests.CSScriptLib/Evaluator.Api.Test.cs b/src/Tests.CSScriptLib/Evaluator.Api.Test.cs index c9418121..6bdca1bf 100644 --- a/src/Tests.CSScriptLib/Evaluator.Api.Test.cs +++ b/src/Tests.CSScriptLib/Evaluator.Api.Test.cs @@ -43,7 +43,7 @@ public int Sum(int a, int b) return a+b; } }", - "MyScript.asm", + new CompileInfo { AssemblyFile = "MyScript.asm" }, out Project project); Assert.True(File.Exists(asmFile)); @@ -101,6 +101,18 @@ string testTempFile(string fileName, [CallerMemberName] string caller = null) return Path.Combine(rootDir, fileName); } + public string testTempDll(string code, string fileName, [CallerMemberName] string caller = null) + { + var rootDir = root.PathJoin(this.GetType().Name, caller).GetFullPath().EnsureDir(); + var filePath = Path.Combine(rootDir, fileName); + // try to avoid unnecessary compilations as xUnint keeps locking the loaded assemblies + if (!fileName.FileExists()) + CSScript.CodeDomEvaluator + .CompileAssemblyFromCode(code, filePath); + + return filePath; + } + public string GetTempScript(string content, [CallerMemberName] string caller = null) { var script = testTempFile("script.cs", $"{this.GetType().Name}.{caller}"); @@ -255,6 +267,126 @@ public class Script eval2.LoadCode($"//css_ref {calcAsm}" + Environment.NewLine + code); } + [Fact] + public void CompileCodeWithRefAliases() + { + var utilAsm = testTempDll( + @"using System; + using System.Reflection; + public class Util + { + public string GetGreeting(string name) + { + return $""Hello, {name}!""; + } + }", + "util.alias.dll"); + + Assembly resolve(object sender, ResolveEventArgs args) + => Assembly.LoadFile(utilAsm); + + AppDomain.CurrentDomain.AssemblyResolve += resolve; + + try + { + // prepare + + var code = @"extern alias util_v1; + using Util1 = util_v1::Util; + using System; + public class Script + { + public string GetGreeting(string name) => new Util1().GetGreeting(name); + }"; + + var eval = this.new_evaluator; + + // test + + dynamic script = new_evaluator.ReferenceAssembly(utilAsm, ["util_v1"]).LoadCode(code); + var result = script.GetGreeting("John"); + + // assert + + Assert.Equal("Hello, John!", result); + } + finally + { + AppDomain.CurrentDomain.AssemblyResolve -= resolve; + } + } + + [Fact] + public void CompileCodeWithRefMultipleVersions() + { + var currentUtilFile = ""; + + Assembly resolve(object sender, ResolveEventArgs args) + => Assembly.LoadFile(currentUtilFile); + + AppDomain.CurrentDomain.AssemblyResolve += resolve; + + try + { + // prepare + var utilCode = @"using System; + using System.Reflection; + [assembly: AssemblyVersion(""1.0.$asmVersion$.0"")] + public class Util + { + public string GetGreeting(string name) + { + var version = this.GetType().Assembly.GetName().Version.ToString(); + return $""Hello, {name} (v{version})!""; + } + }"; + + var utilAsm1 = testTempFile($"util.v0.dll"); + var utilAsm2 = testTempFile($"util.v1.dll"); + + // try to avoid unnecessary compilations as xUnint keeps locking the loaded assemblies + if (!utilAsm1.FileExists()) + CSScript.CodeDomEvaluator + .CompileAssemblyFromCode(utilCode.Replace("$asmVersion$", "0"), + utilAsm1); + + if (!utilAsm2.FileExists()) + CSScript.CodeDomEvaluator + .CompileAssemblyFromCode(utilCode.Replace("$asmVersion$", "1"), + utilAsm2); + + var code = @"using System; + public class Script + { + public string GetGreeting(string name) => new Util().GetGreeting(name); + }"; + + var eval = this.new_evaluator; + eval.IsCachingEnabled = false; + + // test + + eval.Reset(false); + currentUtilFile = utilAsm1; + dynamic script1 = eval.ReferenceAssembly(currentUtilFile).LoadCode(code); + var result1 = script1.GetGreeting("John"); + + eval.Reset(false); + currentUtilFile = utilAsm2; + dynamic script2 = eval.ReferenceAssembly(currentUtilFile).LoadCode(code); + var result2 = script2.GetGreeting("John"); + + // assert + + Assert.Equal("Hello, John (v1.0.0.0)!", result1); + Assert.Equal("Hello, John (v1.0.1.0)!", result2); + } + finally + { + AppDomain.CurrentDomain.AssemblyResolve -= resolve; + } + } + [Fact] public void LoadCode_can_import_scripts() { diff --git a/src/Tests.CSScriptLib/Evaluator.Roslyn.Tests.cs b/src/Tests.CSScriptLib/Evaluator.Roslyn.Tests.cs index 5c8c57e3..709a60bd 100644 --- a/src/Tests.CSScriptLib/Evaluator.Roslyn.Tests.cs +++ b/src/Tests.CSScriptLib/Evaluator.Roslyn.Tests.cs @@ -426,37 +426,73 @@ public class Script CSScript.RoslynEvaluator.CompileAssemblyFromFile(scriptFile, calcAsm); } -#if DEBUG - // [Fact] -#endif + public void Can_use_sassembly_aliases() // manual test + { + var utilAsmFile = @"D:\dev\cs-script\support\#464\util\util\bin\Debug\v1.0.0\util.dll"; + + AppDomain.CurrentDomain.AssemblyResolve += (object sender, ResolveEventArgs args) => + { + if (args.Name.Contains("util")) + { + //return assembly;// NotSupportedException: Resolving to a collectible assembly is not supported. + return Assembly.LoadFrom(utilAsmFile); // will lock the assembly to the current AppDomain so it cannot be unloaded + } + return null; + }; + + // ================================ + var scriptCode = @"extern alias util_v1; + using Util1 = util_v1::Util; + using System; + public class Script + { + public string GetGreeting(string name) => new Util1().GetGreeting(name); + }"; - public void issue_464() // manual test + dynamic script = CSScript.RoslynEvaluator + .ReferenceAssembly(utilAsmFile, ["util_v1"]) + .LoadCode(scriptCode); + + var msg = script.GetGreeting("Bender"); + + Assert.Equal("Hello, Bender (v1.0.0.0)!", msg); + } + + [Fact] + public void issue_464_sidebyside() // manual test { var utilAsmFile1 = @"D:\dev\cs-script\support\#464\util\util\bin\Debug\v1.0.0\util.dll"; var utilAsmFile2 = @"D:\dev\cs-script\support\#464\util\util\bin\Debug\v1.0.1\util.dll"; var utilAsmFile = utilAsmFile1; + var assembly = Assembly.LoadFrom(utilAsmFile); + AppDomain.CurrentDomain.AssemblyResolve += (object sender, ResolveEventArgs args) => { if (args.Name.Contains("util")) { - return Assembly.LoadFrom(utilAsmFile); + // return assembly;// NotSupportedException: Resolving to a collectible assembly is not supported. + // return Assembly.LoadFrom(utilAsmFile); // FileLoadException: 'Could not load file or assembly ''. The located assembly's manifest definition does not match the assembly reference. (0x80131040)' + return Assembly.LoadFile(utilAsmFile); // will lock the assembly to the current AppDomain so it cannot be unloaded } return null; }; - var evaluator = CSScript.Evaluator; - + var evaluator = CSScript.RoslynEvaluator; // ================================ - var scriptCode = @"using System; + var scriptCode = @"extern alias util_v1; + using Util1 = util_v1::Util; + using System; public class Script { - public string GetGreeting(string name) => new Util().GetGreeting(name); + public string GetGreeting(string name) => new Util1().GetGreeting(name); }"; - dynamic script = evaluator.ReferenceAssembly(utilAsmFile) + evaluator.Reset(false); + + dynamic script = evaluator.ReferenceAssembly(utilAsmFile, ["util_v1"]) .LoadCode(scriptCode); var msg = script.GetGreeting("Bender"); @@ -467,10 +503,11 @@ public class Script utilAsmFile = utilAsmFile2; evaluator.Reset(false); - script = evaluator.ReferenceAssembly(utilAsmFile) - .LoadCode(scriptCode); + script = evaluator.ReferenceAssembly(utilAsmFile, ["util_v2"]) + .LoadCode(scriptCode.Replace("util_v1", "util_v2")); msg = script.GetGreeting("Bender"); + Assert.Equal("Hello, Bender (v1.0.1.0)!", msg); } [Fact]