Skip to content

Instantly share code, notes, and snippets.

@altrive
Last active July 23, 2022 06:11
Show Gist options
  • Save altrive/0ce60da4dd58a2d31f4a560c9457bd6f to your computer and use it in GitHub Desktop.
Save altrive/0ce60da4dd58a2d31f4a560c9457bd6f to your computer and use it in GitHub Desktop.

Features

  1. Enable LinqPad Dump() extension method outside of LinqPad by reflection.
  2. Output dump result to HTML with content customizationand
  3. Show HTML content to Chrome browser window.

Requirements

  • LinqPad.exe is installed on default installation path
  • Chrome browser is installedon default installation path

Supported TargetFramework

  • netstandard20 (with .NET Framework runtime)
  • net461
    • following additional NuGet packages required to build
      • System.Runtime.InteropServices.RuntimeInformation
      • System.ValueTuple
  • netcoreapp20
    • Currently don't works. following error occurs.
Unhandled Exception: System.TypeInitializationException: The type initializer for 'LINQPad.UI.Themer' threw an exception. ---> System.TypeLoadException: Could not load type 'System.Drawing.SolidBrush' from assembly 'System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'.
   at LINQPad.UI.Themer..cctor()
   --- End of inner exception stack trace ---
   at LINQPad.UI.Themer.get_IsForcedDark()
   at LINQPad.ObjectGraph.Formatters.XhtmlFormatter.GetHeader()
   at LINQPad.ObjectGraph.Formatters.XhtmlWriter..ctor(TextWriter backingWriter, Boolean enableExpansions, Boolean isInteractive, Boolean fragment, Boolean writeHeader)
   at LINQPad.ObjectGraph.Formatters.XhtmlWriter..ctor(Boolean enableExpansions, Boolean isInteractive, Boolean fragment)
   at LINQPad.Util.CreateXhtmlWriter(Boolean enableExpansions, Int32 maxDepth, Boolean noHeader)
   at CreateXhtmlWriter(Boolean , Int32 , Boolean )
   at System.HtmlDumper.LinqPadHelper.DumpObject[T](T source, Int32 depth, Boolean noheader) in c:\users\takshin\documents\visual studio 2017\Projects\ConsoleApp12\ConsoleApp12\Program.cs:line 168
   at System.HtmlDumper.Dump[T](T this, String title, Int32 maxDepth, String file, Int32 line, String member) in c:\users\takshin\documents\visual studio 2017\Projects\ConsoleApp12\ConsoleApp12\Program.cs:line 85
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Xml.Linq;
namespace ConsoleApp12
{
class Program
{
static void Main(string[] args)
{
//#1: Dump Environment Variables
Environment.GetEnvironmentVariables().Dump("Environment Variables");
//#2: Dump ValueTuple
("key", "value").Dump("ValueTuple");
//#3: Dump AnonymousClass
new { AAA = 1, BBB = 2, CCC = new { DDD = "DDD" } }.Dump("AnonymousClass");
}
}
}
namespace System
{
public static class HtmlDumper
{
private static long _id = 0;
private static object @lock = new object();
public static T Dump<T>(this T @this,
string title = "",
int maxDepth = 5,
[CallerFilePath] string file = "",
[CallerLineNumber] int line = 0,
[CallerMemberName] string member = ""
)
{
var id = Interlocked.Increment(ref _id);
var dateTime = DateTimeOffset.UtcNow;
//Html Dump operation need to be execute with sync
var item = new DumpItem
{
Id = id,
Title = title,
//HtmlFragment = htmlFragment,
//RenderingTime = sw.ElapsedMilliseconds,
CallerContext = new CallerContext
{
AppDomainName = AppDomain.CurrentDomain.FriendlyName,
ThreadId = Thread.CurrentThread.ManagedThreadId,
#if false
//Require NuGet paclage: System.Diagnostics.DiagnosticSource
Activity = Activity.Current?.OperationName,
CorrelationId = Activity.Current?.RootId,
#endif
SynchronizationContextName = SynchronizationContext.Current?.ToString() ?? "",
MemberName = member,
FilePath = file,
LineNumber = line,
},
Timestamp = dateTime,
};
string html = null;
//Step1: Create Dump HTML by LinqPad
var stopwatch = Stopwatch.StartNew();
{
html = LinqPadHelper.DumpObject(@this, maxDepth);
}
item.RenderingTime = stopwatch.ElapsedMilliseconds;
//Step2: Rewrite HTML
stopwatch.Restart();
{
html = HtmlHelper.Rewrite($"#{id}:{title}", html, item);
}
//Debug.WriteLine($"Rewrite Time: {stopwatch.ElapsedMilliseconds}[ms]");
var tempFileName = IndexHtmlPath.Replace("Index.html", "Index" + id.ToString("0000") + ".html");
lock (@lock)
{
//Step3: Write HTML File to %Temp%\HtmlDumper\IndexNNNN.html
File.WriteAllText(tempFileName, html);
//Step4: Launch chrome window
ChromeLauncher.LaunchNewWindow(tempFileName, ChromeProfilePath);
}
return @this;
}
private static string BasePath
{
get
{
var path = Path.Combine(Path.GetTempPath(), "HtmlDumper");
new DirectoryInfo(path).Create(); //Ensure directory exists
return path;
}
}
private static string IndexHtmlPath => Path.Combine(BasePath, "Index.html");
private static string ChromeProfilePath = Path.Combine(BasePath, "ChromeProfile");
[Serializable]
internal class DumpItem
{
public long Id { get; set; }
//public DumpType Type { get; set; }
public string Title { get; set; }
public string HtmlFragment { get; set; }
public long RenderingTime { get; internal set; }
//Other caller contex related data
public CallerContext CallerContext { get; set; }
//TODO: Need to specify JSON Serialization format
public DateTimeOffset Timestamp { get; set; }
}
[Serializable]
internal class CallerContext
{
public string AppDomainName { get; set; }
public int ThreadId { get; set; }
//public string Activity { get; set; }
//public string CorrelationId { get; set; }
public string SynchronizationContextName { get; set; }
public string MemberName { get; set; }
public string FilePath { get; set; }
public int LineNumber { get; set; }
//Helper Properties
public bool HasSynchronizationContext => !String.IsNullOrEmpty(SynchronizationContextName);
}
private class LinqPadHelper
{
public static string DumpObject<T>(T source, int depth = 5, bool noheader = false)
{
//Note: Error occurs when call LinqPad method from .NET Core
if (RuntimeInformation.FrameworkDescription.StartsWith(".NET Core"))
throw new NotSupportedException(".NET Core platform is not supported when using LinqPad's HTML Dump method");
//TODO: XhtmlWriter share internal state (example: table id, totalTextLengh .etc)
var createXhtmlWriter = lazyCache.Value;
using (var writer = createXhtmlWriter(true, depth, noheader))
{
// TODO: XhtmlWriter.Write has internal size limit (totalTextLenth: 10000000 || totalObjects > 110000)
// It may be better using XhtmlWriter.FormatObject instead.
writer.Write(source);
return writer.ToString();
}
}
//Delegate for Util.CreateXhtmlWriter
private static Lazy<Func<bool, int, bool, TextWriter>> lazyCache = new Lazy<Func<bool, int, bool, TextWriter>>(() =>
{
var type = LinqPadAssembly.Value.GetType("LINQPad.Util");
var method = new DynamicMethod(
"CreateXhtmlWriter",
typeof(TextWriter),
new Type[] { typeof(bool), typeof(int), typeof(bool) },
type,
true
);
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.Emit(OpCodes.Call, type.GetMethod("CreateXhtmlWriter", new Type[] { typeof(bool), typeof(int), typeof(bool) }));
il.Emit(OpCodes.Ret);
//Cache Util.CreateHtmlWriter delegate
return (Func<bool, int, bool, TextWriter>)method.CreateDelegate(typeof(Func<bool, int, bool, TextWriter>));
});
/// <summary>
/// Load Linqpad assembly
/// </summary>
private static Lazy<Assembly> LinqPadAssembly = new Lazy<Assembly>(() =>
{
//Helper method
string GetLinqPadExePath(Environment.SpecialFolder folder) => Path.Combine(Environment.GetFolderPath(folder), @"LINQPad5\LINQPad.exe");
//1. Search LinqPad5 AnyCPU Build instalation path
if (Environment.Is64BitProcess)
{
var linqPadExePath = GetLinqPadExePath(Environment.SpecialFolder.ProgramFiles);
if (File.Exists(linqPadExePath))
return Assembly.Load(File.ReadAllBytes(linqPadExePath)); //Load from bytes to avoid assembly file lock
}
//2. Search LinqPad5 default instalation path
{
var linqPadExePath = GetLinqPadExePath(Environment.SpecialFolder.ProgramFilesX86);
if (File.Exists(linqPadExePath))
return Assembly.Load(File.ReadAllBytes(linqPadExePath));
}
//If LinqPad.exe is not found at expected path
throw new FileNotFoundException(String.Join(Environment.NewLine,
new[] {
"LINQPad.exe is not found at following locations",
" " + GetLinqPadExePath(Environment.SpecialFolder.ProgramFiles),
" " + GetLinqPadExePath(Environment.SpecialFolder.ProgramFilesX86)
}.Distinct() //Need distinct when called from 32bit process
));
});
}
private static class HtmlHelper
{
// Use Base64 favicon. from <https://www.freefavicon.com/freefavicons/software/iconinfo/zoom-fit-best-152-195665.html>
const string base64Icon = @"data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAIjkhFGCSTgAAAA8AAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUdrcHKY3PvDOX2fwkh8hlAA0XAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOdroEKpHUq1C09f9LsPD/PKDh/SCDwzkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHb7YBKJDUmWfM//9my///XcH//y+U1bwegMENAAAAAAAAAAAAAAAAAAAAAAAAAACzsrIBAAAAAAAAAAAAAAAAIIjLcoDk//+D6P//d9z//zKY2rAWerwIAAAAAAAAAAAAAAAAq62vFbu7vInExMTXx8bG8czKyt7IxsWZyMXENcLY6M+B2v//kvL//zyg4qQNdLgFAAAAAAAAAAAAAAAAr7GzM8XGyOjX19f/0NDQ/8zLy//V1NT/4d/e/9fV1P/i4N7/xdjl2DSX2HoGb7UCAAAAAAAAAAAAAAAAra+wEsvMzuTd3t7/7tO9///zsf///6////mw///duv/j4eH/4d/e/8jGxEAAAAAAAAAAAAAAAAAAAAAAAAAAAL/Bwn3n6On/7dK8///////////////i/////////////9qv//Px8P/U0tGkAAAAAAAAAAAAAAAAAAAAAAAAAADV1tfG7O3u///qq//////////9////w////9r////////wmf/r5+b/5OLh67y6uQIAAAAAAAAAAAAAAAAAAAAA4+Tl3ezt7f///7n////6////4P///9X////M////8///+57/8eff//Du7vq6uLcKAAAAAAAAAAAAAAAAAAAAAOXl5sX///////zG///////////////w////+P////////uo//n39f/w7u7qtbOzAgAAAAAAAAAAAAAAAAAAAADQ0dF7///////t3P/////////////////////////////pwv//////4N/fogAAAAAAAAAAAAAAAAAAAAAAAAAAtre3Efr7++P///////bk////5P///+P////N///wzv//////////9LOztCYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC+v78w////5f//////////////////////////////8snJyksAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALe4uRTo6eqF////0v///+3////Z8fHylLa3tyEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAP/gAAD/wAAA/4AAAPuBAADAAwAAgAcAAAAfAAAAHwAAAA8AAAAPAAAADwAAAB8AAAAfAACAPwAAwH8AAA==";
public static string Rewrite(string title, string xhtml, DumpItem item)
{
var doc = XDocument.Parse(xhtml);
doc.Element("html")
.Element("head")
.Add(new XElement("title", title),
new XElement("link", new XAttribute("href", base64Icon),
new XAttribute("rel", "icon"),
new XAttribute("type", "image/x-icon")));
//Insert extra data
doc.Element("html")
.Element("body")
.AddFirst(XElement.Parse(ToTableHtml(new[] { item })));
xhtml = doc.ToString(SaveOptions.DisableFormatting);
return xhtml;
}
private static string ToTableHtml(DumpItem[] items)
{
var sb = new StringBuilder(4096);
sb.AppendLine("<table class='table'>")
.AppendLine("<thead>")
.AppendLine(" <th>Id</th>")
.AppendLine(" <th>Title</th>")
.AppendLine(" <th>ThreadId</th>")
.AppendLine(" <th>SynchronizationContext</th>")
.AppendLine(" <th>MemberName</th>")
.AppendLine(" <th>FilePath</th>")
.AppendLine(" <th>LineNumber</th>")
.AppendLine(" <th>RenderTime</th>")
.AppendLine("</thead>")
.AppendLine("<tbody>");
foreach (var item in items)
{
sb.AppendLine($"<tr>")
.AppendLine($" <th>#{item.Id}</th>")
.AppendLine($" <td>{item.Title}</td>")
.AppendLine($" <td>{item.CallerContext.ThreadId}</td>")
.AppendLine($" <td>{item.CallerContext.SynchronizationContextName}</td>")
.AppendLine($" <td>{item.CallerContext.MemberName}</td>")
.AppendLine($" <td title='{item.CallerContext.FilePath}'>{Path.GetFileName(item.CallerContext.FilePath)}</td>")
.AppendLine($" <td>{item.CallerContext.LineNumber}</td>")
.AppendLine($" <td>{item.RenderingTime} [ms]</td>")
.AppendLine($"</tr>");
}
sb.AppendLine("</tbody>")
.AppendLine("</table>");
return sb.ToString();
}
}
private static class ChromeLauncher
{
public static void LaunchNewWindow(string url, string chormeProfilePath)
{
var chromeExePath = ChromeExePath.Value;
if (chromeExePath == null)
throw new FileNotFoundException("Couldn't find Chrome installation!");
string args = String.Join(" "
, url
//, "--app=" + url
, "--user-data-dir=" + chormeProfilePath
//, "--remote-debugging-port=9222"
//, "--allow-file-access-from-files"
);
var psi = new ProcessStartInfo(chromeExePath, args);
var process = Process.Start(psi);
process.WaitForInputIdle();
}
private static Lazy<string> ChromeExePath = new Lazy<string>(() =>
{
//TODO: Support non Windows environment (after removing LinqPad.exe dependency)
//TODO: Search exe path from following registry. but it require NuGet package "Microsoft.Win32.Registry"
//@"\Software\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe";
//Helper method
string GetChromeExePath(Environment.SpecialFolder folder) => Path.Combine(Environment.GetFolderPath(folder), @"Google\Chrome\Application\chrome.exe");
//1. Search "ProgramFiles (x86)" default instalation path (It seems 64 bit chrome use same path)
{
var chromeExePath = GetChromeExePath(Environment.SpecialFolder.ProgramFilesX86);
if (File.Exists(chromeExePath))
return chromeExePath;
}
//2. Search LocalAppData installation path
{
var chromeExePath = GetChromeExePath(Environment.SpecialFolder.LocalApplicationData);
if (File.Exists(chromeExePath))
return chromeExePath;
}
//3. Search "ProgramFiles" instalation path (Should not be used)
if (Environment.Is64BitProcess)
{
var chromeExePath = GetChromeExePath(Environment.SpecialFolder.ProgramFiles);
if (File.Exists(chromeExePath))
return chromeExePath;
}
//If chrome.exe is not found at expected paths
throw new FileNotFoundException(String.Join(Environment.NewLine,
new[] {
"chrome.exe is not found at following expected locations",
" " + GetChromeExePath(Environment.SpecialFolder.ProgramFilesX86),
" " + GetChromeExePath(Environment.SpecialFolder.LocalApplicationData),
" " + GetChromeExePath(Environment.SpecialFolder.CommonProgramFiles)
}.Distinct() //Need distinct called by 32bit process
));
});
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment