One missing feature from LinqPad is the ability to easily reuse code in a query we wrote in other queries.
If we really want to do it we have the following options:
Copy and paste the code into the new query
Open Visual Studio, create a project, create a class and copy the code you want to reuse. Compile the project and finally reference the generated dll from your query.
Compile the LinqPad query directly (using a special script), then reference the generated dll from your query.
Today we will look at the third option. Found in the .Net framework is a very interesting API called the CodeDOM API. This API can be used to compile .Net code at run-time, like the compiler does. With this, we will be able to parse a LinqPad query and compile it.
This is what we will look into right now.
Compiling a LinqPad query
Those are the steps we need to do to accomplish query compilation:
Read the query and extract the options, usings and references
Create the CodeDOM objects and set the options and references
Create a string of the code file including the usings and wrap the query code inside a class
Compile the code
I'll explain in details each steps in future blog posts but for now I'll give you my query to compile LinqPad queries. You will notice that this is a self compiling query as the query compile itself to a dll I can reuse it to compile other queries!
You can figure out the Compiler class usage from the Main function for now.
Additional Namespace Imports System.CodeDom.Compiler
System.Runtime.InteropServices
Microsoft.CSharp
Query (C# Program)
void Main()
{
Environment.CurrentDirectory = Path.GetDirectoryName(Util.CurrentQueryPath);
Compiler.CompileFiles(Options.CreateOnDiskDll(
@namespace: "LinqPad.QueriesCompiler",
outputFile: "LinqPad.QueriesCompiler.dll")
.AddCodeFile(CodeType.LinqProgramTypes, Util.CurrentQueryPath))
.Dump("Successfully created assembly at " + DateTime.Now.ToLocalTime());
}
// Define other methods and classes here
public static class Compiler
{
public static Assembly CompileFiles(Options options)
{
var outputPath = options.IsInMemory ? "" : options.OutputFile;
if (!options.CodeFiles.Any())
throw new InvalidOperationException("Should add at least one file to compile.");
foreach (var codeFile in options.CodeFiles)
{
codeFile.RawContent = File.ReadAllLines(codeFile.FilePath);
codeFile.Query = GetQuery(new FileInfo(codeFile.FilePath).DirectoryName, codeFile.RawContent);
GetCode(codeFile, options.Namespace);
}
return BuildAssembly(options.CodeFiles, options, outputPath);
}
public static Query GetQuery(string folder, IEnumerable<string> content)
{
var xml = string.Join("\r\n", content.TakeWhile(l => l.Trim().StartsWith("<")));
var queryElement = XDocument.Parse(xml).Element("Query");
if (queryElement == null) throw new InvalidOperationException("Missing <Query> header definition");
var query = new Query
{
Kind = queryElement.Attribute("Kind").Value,
Namespaces = queryElement.Elements("Namespace").Select(n => n.Value).ToList(),
GACReferences = queryElement.Elements("GACReference").Select(n => n.Value).ToList(),
RelativeReferences = queryElement.Elements("Reference").Where(e => e.Attribute("Relative") != null)
.Select(n => n.Attribute("Relative").Value)
.Select(x => new FileInfo(Path.Combine(folder, x)).FullName)
.ToList(),
OtherReferences = queryElement.Elements("Reference").Where(e => e.Attribute("Relative") == null)
.Select(n => n.Value.Replace("<RuntimeDirectory>", RuntimeEnvironment.GetRuntimeDirectory()))
.Select(n => n.Replace("<ProgramFilesX86>", Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86)))
.ToList()
};
return query;
}
public static IEnumerable<string> GetCode(IEnumerable<IEnumerable<string>> contents, Query query, string ns)
{
return contents.Select(c => GetCode(c, query, ns));
}
private static void GetCode(CodeFile codeFile, string @namespace)
{
IEnumerable<string> result = null;
if (codeFile.Query.Kind != "Program" && codeFile.Query.Kind != "Statements")
throw new InvalidOperationException("Only queries of type C# program and C# statements are supported");
var filteredContent = codeFile.RawContent.SkipWhile(l => l.Trim().StartsWith("<"));
switch (codeFile.Type)
{
case CodeType.LinqStatements:
result = WrapInClass(WrapInMain(filteredContent));
break;
case CodeType.LinqProgramTypes:
result = FilterTypes(filteredContent);
break;
case CodeType.LinqProgram:
result = WrapInClass(filteredContent);
break;
default:
throw new InvalidOperationException("Only queries of type C# program and C# statements are supported");
}
codeFile.Content = GetCode(result, codeFile.Query, @namespace);
}
private static IEnumerable<string> WrapInClass(IEnumerable<string> inputCode)
{
var s = new[] { "public class Program {" };
var e = new[] { "}" };
return s.Concat(inputCode).Concat(e);
}
private static IEnumerable<string> WrapInMain(IEnumerable<string> inputCode)
{
var s = new[] { "public static void Main() {" };
var e = new[] { "}" };
return s.Concat(inputCode).Concat(e);
}
private static IEnumerable<string> FilterTypes(IEnumerable<string> inputCode)
{
return inputCode.SkipWhile(l => l.Trim() != "// Define other methods and classes here");
}
public static string GetCode(IEnumerable<string> content, Query query, string ns)
{
var code = string.Join(Environment.NewLine, content);
var codeBuilder = new StringBuilder();
codeBuilder.AppendLine("using " + string.Join(";\r\nusing ", query.Namespaces.Union(StandardNamespaces)) + ";");
codeBuilder.AppendLine(string.Format("namespace {0} {{", ns));
codeBuilder.AppendLine(code);
codeBuilder.AppendLine("}");
return codeBuilder.ToString();
}
public static Assembly BuildAssembly(IEnumerable<CodeFile> codeFiles, Options options, string outputPath)
{
var providerOptions = new Dictionary<string, string> { { "CompilerVersion", "v4.0" } };
var provider = new CSharpCodeProvider(providerOptions);
var assemblies = new[]
{
codeFiles.SelectMany(c => c.Query.GACReferences.Select(s => Assembly.Load(s).Location)),
codeFiles.SelectMany(c => c.Query.RelativeReferences.Select(s => Assembly.LoadFrom(s).Location)),
codeFiles.SelectMany(c => c.Query.OtherReferences),
Assemblies
};
var compilerparams = new CompilerParameters
{
GenerateExecutable = !string.IsNullOrWhiteSpace(options.StartupObject),
OutputAssembly = options.IsInMemory ? null : outputPath,
GenerateInMemory = true,
IncludeDebugInformation = true,
MainClass = options.StartupObject
};
compilerparams.ReferencedAssemblies.AddRange(assemblies.SelectMany(a => a).ToArray());
var results = provider.CompileAssemblyFromSource(compilerparams, codeFiles.Select(f => f.Content).ToArray());
if (results.Errors.HasErrors)
{
var errors = new StringBuilder("Compiler Errors:\r\n");
foreach (CompilerError error in results.Errors)
{
errors.AppendFormat("File {0}, Line {1},{2}\t: {3}\r\n",
error.FileName, error.Line, error.Column, error.ErrorText);
}
throw new Exception("Errors compiling:\r\n" + errors + "\r\n\r\n" +
string.Join(Environment.NewLine, codeFiles.Select(f => f.FilePath + ":\r\n" + AddLineNumber(f.Content))));
}
return results.CompiledAssembly;
}
private static string AddLineNumber(string code)
{
var lines = code.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
return string.Join(Environment.NewLine, lines.Select ((x, i) => (i + 1).ToString().PadLeft(4) + ": " + x));
}
private static List<string> StandardNamespaces
{
get
{
return new List<string>
{
"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.Xml",
"System.Xml.Linq",
"System.Xml.XPath"
};
}
}
private static List<string> Assemblies
{
get
{
return new List<string>
{
"System.dll",
"System.Core.dll",
"System.Data.dll",
"System.Xml.dll",
"System.Xml.Linq.dll",
"System.Data.Linq.dll",
"System.Drawing.dll",
"System.Data.DataSetExtensions.dll"
};
}
}
}
public class Query
{
public string Kind { get; set; }
public List<string> Namespaces { get; set; }
public List<string> GACReferences { get; set; }
public List<string> RelativeReferences { get; set; }
public List<string> OtherReferences { get; set; }
}
public enum CodeType
{
LinqStatements, // Wrap inside class and Main() method
LinqProgramTypes, // Code after 'Define other methods and classes here' directly inside namespace
LinqProgram, // Wrap all code inside class
}
public class CodeFile
{
public CodeType Type { get; set; }
public string FilePath { get; set; }
public string[] RawContent { get; set; }
public Query Query { get; set; }
public string Content { get; set; }
}
public class Options
{
private Options(string @namespace)
{
if (string.IsNullOrWhiteSpace(@namespace)) throw new ArgumentNullException("namespace");
this.CodeFiles = new List<CodeFile>();
this.Namespace = @namespace;
}
public static Options CreateInMemoryDll(string @namespace)
{
var options = new Options(@namespace);
options.OutputFile = null;
options.IsInMemory = true;
options.StartupObject = null;
return options;
}
public static Options CreateOnDiskDll(string @namespace, string outputFile)
{
if (string.IsNullOrWhiteSpace(outputFile)) throw new ArgumentNullException("outputFile");
var options = new Options(@namespace);
options.OutputFile = outputFile;
options.IsInMemory = false;
options.StartupObject = null;
return options;
}
public static Options CreateOnDiskExe(string @namespace, string outputFile, string startupObject)
{
if (string.IsNullOrWhiteSpace(outputFile)) throw new ArgumentNullException("outputFile");
if (string.IsNullOrWhiteSpace(startupObject)) throw new ArgumentNullException("startupObject");
var options = new Options(@namespace);
options.OutputFile = outputFile;
options.IsInMemory = false;
options.StartupObject = startupObject;
return options;
}
public Options AddCodeFile(CodeType type, string filePath)
{
this.CodeFiles.Add(new CodeFile { Type = type, FilePath = filePath });
return this;
}
public string Namespace { get; private set; }
public string OutputFile { get; private set; }
public List<CodeFile> CodeFiles { get; private set; }
public string StartupObject { get; private set; }
public bool IsInMemory { get; private set; }
}