the overengineer

Hacky, quick-fix debugging of .NET Core in Visual Studio

• blog

In the latest .NET Core there is currently no compatible Visual Studio tooling for debugging from within VS.

This is due to a shift from DNX to CLI, and on-going work.

Anyway, that’s dull, let’s cut to the chase. Want some primitive debugging support to your .NET Core stuff?

Step 1 - Install the latest CLI

https://dotnetcli.blob.core.windows.net/dotnet/beta/Installers/Latest/dotnet-dev-win-x64.latest.exe

Step 2 - Install Visual Commander VS extension

Install: https://visualstudiogallery.msdn.microsoft.com/deda8ac1-75e6-4068-89ab-b607cee38f2d

Info: https://vlasovstudio.com/visual-commander/

Step 3 - Import my debug and run commands

https://www.dropbox.com/s/tq8bqp00pvja3w6/netcoredebugging.vcmd?dl=0

The (current at the time of writing) code for the commands is below.

The code for both are identical, with the exception that the following line is commented out in the “Run” version:

   	// Comment out this line if you want just "Run" functionality
	proc.Attach();

This is the line that attaches VS debugging to the process, so without it it’s just Ctrl+F5 type “Run”.

The commands expect to find Newtonsoft.Json.dll somewhere, notably here:

C:\Program Files (x86)\Common Files\Microsoft Shared\VsHub\1.0.0.0\lib\Newtonsoft.Json.dll

You can edit the commands yourself if you want to change the location of that DLL (or anything else).

Finally, if you want to have debugging right from your entry point (e.g. Program.cs) you need to add this:

public static void Main(string[] args)
{
    args = DotNetRunner.AppStart(args);
    ...
}

Where the DotNetRunner.cs looks like this:

public static class DotNetRunner
{
	public static string[] AppStart(params string[] args)
	{
		var newArgs = new List<string>();
		if (args.Length > 0)
		{
			var prefix = "wait:";
			foreach (var arg in args)
			{
				if (arg.StartsWith(prefix))
				{
					var wait = Int32.Parse(arg.Substring(prefix.Length));
					Thread.Sleep(wait);
				}
				else
				{
					newArgs.Add(arg);
				}
			}
		}
		return newArgs.ToArray();
	}
}

This is a very, VERY hacky hack hack that simply tells the app to sleep for 5 seconds, otherwise we don’t attach debugging in time to jump into the entry point’s code. You can adjust the 5 seconds to someting else as suited by changing the “wait:5000” in the command code to something else if you need. Yes, it’s horrible, truly horrible, but until the Visual Studio CLI tooling is here, it’s fine.

So, overall this is VERY primitive, but here’s what it does:

Code for the Visual Commander command(s):

using EnvDTE;
using EnvDTE80;
using System;
using System.IO;
using System.Windows.Forms;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public class C : VisualCommanderExt.ICommand
{
	public string ProjectPath { get; set; }

	public void Run(EnvDTE80.DTE2 DTE, Microsoft.VisualStudio.Shell.Package package)
	{
		DTE.Documents.SaveAll();
		var startupProjectFile = GetStartupProject(DTE);
		ProjectPath = Path.GetDirectoryName(startupProjectFile);
		var projectName = Path.GetFileName(ProjectPath);
		var vcmdFolder = Path.GetFullPath(Path.Combine(ProjectPath, "..\\.vcmd"));

		SetHidden(vcmdFolder);

		var outputFolder = Path.Combine(vcmdFolder, projectName);
		outputFolder = Path.GetFullPath(Path.Combine(ProjectPath, outputFolder));
		var exe = Path.Combine(outputFolder, projectName) + ".exe";

		TryRun("onbeforerun.bat", true);

		var existingProcesses = System.Diagnostics.Process.GetProcessesByName(projectName);
		foreach (var existingProcess in existingProcesses)
		{
			existingProcess.Kill();
		}

		// Call "dotnet restore"
		DotNet("restore");

		// Call "dotnet build"
		DotNet("publish -c Debug -o \"" + outputFolder + "\"");

		var process = new System.Diagnostics.Process();
		process.OutputDataReceived += (sender, args) =>
		{
			var line = args.Data;
			if (line == null)
			{
				return;
			}
			var prefix = "Now listening on: ";
			if (line.Trim().StartsWith(prefix))
			{
				var url = line.Substring(prefix.Length);
				var launchSettingsPath = Path.Combine(ProjectPath, @"properties\launchSettings.json");
				if (File.Exists(launchSettingsPath))
				{
					JObject obj = JObject.Parse(File.ReadAllText(launchSettingsPath));
					var settingsUrl = "";
					try
					{
						JToken token = obj["profiles"]["IIS Express"]["launchUrl"];
						settingsUrl = token.Value<string>();
					}
					catch (NullReferenceException e)
					{
					}
					if (!string.IsNullOrWhiteSpace(settingsUrl))
					{
						if (IsAbsoluteUrl(settingsUrl))
						{
							url = settingsUrl;
						}
						else
						{
							url = url.TrimEnd('/') + "/" + settingsUrl.TrimStart('/');
						}
					}
					url = url.Trim();
				}
				if (!string.IsNullOrWhiteSpace(url))
				{
					System.Diagnostics.Process.Start("explorer", "\"" + url + "\"");
				}
			}
		};
		var ps = new System.Diagnostics.ProcessStartInfo(exe, "wait:5000");
		ps.RedirectStandardOutput = true;
		ps.UseShellExecute = false;
		process.EnableRaisingEvents = true;
		process.StartInfo = ps;
		process.Start();
		process.BeginOutputReadLine();

		TryRun("onrun.bat");

		foreach (Process proc in DTE.Debugger.LocalProcesses)
		{
			if (proc.ProcessID == process.Id)
			{
				proc.Attach();
				return;
			}
		}
	}

	void SetHidden(string folder)
	{
		if (!Directory.Exists(folder))
		{
			Directory.CreateDirectory(folder);
		}

		DirectoryInfo di = new DirectoryInfo(folder);

		// See if directory has hidden flag, if not, make hidden
		if ((di.Attributes & FileAttributes.Hidden) != FileAttributes.Hidden)
		{
			// Add Hidden flag    
			di.Attributes |= FileAttributes.Hidden;
		}
	}

	bool IsAbsoluteUrl(string url)
	{
		Uri result;
		return Uri.TryCreate(url, UriKind.Absolute, out result);
	}

	public void TryRun(string batch, bool waitForExit = false)
	{
		var onrun = Path.Combine(ProjectPath, batch);
		if (File.Exists(onrun))
		{
			var process = new System.Diagnostics.Process();
			var psi = new System.Diagnostics.ProcessStartInfo(onrun);
			psi.WorkingDirectory = ProjectPath;
			process.StartInfo = psi;
			process.Start();
			if (waitForExit)
			{
				process.WaitForExit();
			}
		}
	}

	public void DotNet(string command)
	{
		var process = new System.Diagnostics.Process();
		var ps = new System.Diagnostics.ProcessStartInfo("cmd", "/k call dotnet " + command + " & exit");
		ps.WorkingDirectory = ProjectPath;
		process.StartInfo = ps;
		process.Start();
		process.WaitForExit();
	}

	public string GetStartupProject(EnvDTE80.DTE2 DTE)
	{
		var solutionPath = Path.GetDirectoryName(DTE.Solution.FullName);
		var projectFile = "";
		foreach (String s in (Array)DTE.Solution.SolutionBuild.StartupProjects)
		{
			projectFile = Path.Combine(solutionPath, s);
			break;
		}
		return projectFile;
	}
}