Last active
October 18, 2018 00:33
-
-
Save ronnieoverby/57c649eca6039985d242f575a2c1d22c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Query Kind="Program"> | |
<NuGetReference>Microsoft.CodeAnalysis.CSharp</NuGetReference> | |
<Namespace>LINQPad.ObjectModel</Namespace> | |
<Namespace>Microsoft.CodeAnalysis</Namespace> | |
<Namespace>Microsoft.CodeAnalysis.CSharp</Namespace> | |
<Namespace>Microsoft.CodeAnalysis.Emit</Namespace> | |
<Namespace>static LINQPad.Util</Namespace> | |
<Namespace>static System.IO.Path</Namespace> | |
<Namespace>static System.Reflection.Assembly</Namespace> | |
<Namespace>static System.Reflection.BindingFlags</Namespace> | |
<Namespace>Microsoft.CodeAnalysis.CSharp.Syntax</Namespace> | |
</Query> | |
void Main() | |
{ | |
var query = LoadQuery("test"); | |
GetQueryResult<int>(query, false).Dump(); | |
} | |
T GetQueryResult<T>(Query query, bool dumpCode = false, IEnumerable<string> defaultReferences = null, IEnumerable<string> defaultNamespaceImports = null) | |
{ | |
if (!query.IsCSharp) | |
throw new NotSupportedException("Query must be written in C#"); | |
if (query.GetConnectionInfo() != null) | |
throw new NotSupportedException("Connected queries are not supported."); | |
var ClassName = CreateCSharpIdentifier(); | |
const string Main = nameof(Main); | |
var namespaces = query.NamespaceImports.Union(defaultNamespaceImports ?? new[] { | |
"System", | |
"System.IO", | |
"System.Text", | |
"System.Text.RegularExpressions", | |
"System.Diagnostics", | |
"System.Threading", | |
"System.Reflection", | |
"System.Collections", | |
"System.Collections.Generic", | |
"System.Linq", | |
"System.Linq.Expressions", | |
"System.Data", | |
"System.Data.SqlClient", | |
"System.Data.Linq", | |
"System.Data.Linq.SqlClient", | |
"System.Transactions", | |
"System.Xml", | |
"System.Xml.Linq", | |
"System.Xml.XPath", | |
"LINQPad" | |
}).OrderBy(x => x); | |
string GetCode() | |
{ | |
if (query.Language == QueryLanguage.Expression) | |
return $"public {typeof(T).FullName} {Main}() => {query.Text};"; | |
if (query.Language == QueryLanguage.Program) | |
return query.Text; | |
throw new NotSupportedException($"Query language \"{query.Language}\" is not supported."); | |
} | |
var syntaxTree = CSharpSyntaxTree.ParseText( | |
$"{string.Concat(namespaces.Select(n => $"using {n};"))} public class {ClassName} {{ {GetCode()} \r\n}}"); | |
if (dumpCode) | |
syntaxTree.GetRoot().NormalizeWhitespace().ToFullString(); | |
var assemblyName = GetRandomFileName(); | |
var assemblies = ((defaultReferences ?? new[] { | |
"mscorlib", | |
"netstandard", | |
"System", | |
"Microsoft.CSharp", | |
"System.Core", | |
"System.Data", | |
"System.Transactions", | |
"System.Xml", | |
"System.Xml.Linq", | |
"System.Data.Linq", | |
"System.Drawing", | |
"System.Data.DataSetExtensions", | |
"System.Text.Encoding", | |
"System.Threading.Tasks", | |
"LINQPad" | |
}).Select(Load) | |
).Concat( | |
query.FileReferences.Select(LoadFile) | |
).Concat( | |
from n in query.NuGetReferences | |
from r in n.GetAssemblyReferences() | |
select LoadFile(r) | |
).Concat( | |
query.GacReferences.Select(Load) | |
); | |
var compilation = CSharpCompilation.Create( | |
assemblyName, | |
syntaxTrees: new[] { syntaxTree }, | |
references: assemblies.Select(a => MetadataReference.CreateFromFile(a.Location)), | |
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); | |
using (var ms = new MemoryStream()) | |
{ | |
var result = compilation.Emit(ms); | |
if (!result.Success) | |
{ | |
result.Diagnostics.Where(diag => | |
diag.IsWarningAsError || | |
diag.Severity == DiagnosticSeverity.Error).Dump("Emit Failures"); | |
throw new Exception("Emit failure"); | |
} | |
var assembly = Load(ms.ToArray()); | |
var type = assembly.GetType(ClassName); | |
var obj = Activator.CreateInstance(type); | |
var mainMethods = type.GetMethods(Public | NonPublic | Instance) | |
.Where(m => m.Name == Main && typeof(T).IsAssignableFrom( m.ReturnType )) | |
.Take(2).ToArray(); | |
if (mainMethods.Length != 1) | |
throw new Exception($"Cannot find {Main} method with return type {typeof(T).FullName}"); | |
var mainMethod = mainMethods.Single(); | |
// mimic linqpad behavior of supplying default values to Main(…) | |
var defaultParams = from p in mainMethod.GetParameters() | |
select GetDefault(p.ParameterType); | |
var instance = (T)mainMethod.Invoke(obj, defaultParams.ToArray()); | |
return instance; | |
} | |
} | |
Dictionary<Type, object> _defaultValues = new Dictionary<Type, object>(); | |
object GetDefault(Type t) | |
{ | |
if (!t.IsValueType) | |
return null; | |
if (_defaultValues.TryGetValue(t, out object value)) | |
return value; | |
var exprDefault = Expression.Default(t); | |
var castObject = Expression.Convert(exprDefault, typeof(object)); | |
var lambda = Expression.Lambda<Func<object>>(castObject); | |
var f = lambda.Compile(); | |
return _defaultValues[t] = f(); | |
} | |
Query LoadQuery(string nameOrPath) | |
{ | |
var path = new Func<string>[] | |
{ | |
() => nameOrPath, | |
() => Combine(MyQueriesFolder, ChangeExtension(GetFileName(nameOrPath), ".linq")) | |
} | |
.Select(f => f()) | |
.FirstOrDefault(File.Exists); | |
if (path == null) | |
throw new FileNotFoundException(nameOrPath); | |
var query = Query.Load(path); | |
return query; | |
} | |
string CreateCSharpIdentifier() => $"_{Guid.NewGuid():n}"; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment