Macross.Windows.Debugging is a .NET Core 3.1+ library which will spawn a WinForms UI when debugging on Windows to present ILogger messages as they are written by the application in a more friendly way than what the default VS debug console will do. No external tools necessary.
Example:
- Auto-start when debugger is attached or through IConfiguration.
- Displays messages in tabs by category or group.
- Tabs can be hidden.
- Messages, associated data, and scopes are flattened into JSON.
- Automatic tailing of messages.
- Start as window or minimized.
- Minimize to taskbar or system tray.
- Window and tabs are fully customizable.
- ...and more!
- Given this is a debug tool, it should have no impact on production execution. A lot of effort was put into making sure the hooks are not put into the logging pipeline unless the UI is being used.
- The UI should remain responsive even when high-volume messages are being written.
- The UI shouldn't dominate the resources available to the process.
Getting the DebugWindow integrated into your application is simple, but some
care has to be taken to compile it just for Windows.
-
In your
csprojadd aWINDOWSconstant, aTargetFrameworkfornet5.0-windows, and conditional target on yourMacross.Windows.Debuggingreference:Single TargetFramework:
<PropertyGroup Condition="'$(OS)' == 'Windows_NT'"> <DefineConstants>WINDOWS</DefineConstants> <TargetFramework>net5.0-windows</TargetFramework> </PropertyGroup> <PropertyGroup Condition="'$(OS)' != 'Windows_NT'"> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="Macross.Windows.Debugging" Version="1.3.1" Condition="$(OS) == 'Windows_NT'" /> </ItemGroup>
Multiple TargetFrameworks:
<PropertyGroup> <TargetFrameworks>netcoreapp3.1</TargetFrameworks> </PropertyGroup> <PropertyGroup Condition="'$(OS)' == 'Windows_NT'"> <TargetFrameworks>$(TargetFrameworks);net5.0-windows</TargetFrameworks> <DefineConstants>WINDOWS</DefineConstants> </PropertyGroup> <PropertyGroup Condition="'$(OS)' != 'Windows_NT'"> <TargetFrameworks>$(TargetFrameworks);net5.0</TargetFrameworks> </PropertyGroup> <ItemGroup> <PackageReference Include="Macross.Windows.Debugging" Version="1.3.1" Condition="$(OS) == 'Windows_NT'" /> </ItemGroup>
-
Example net5.0 csproj available. (TBD)
-
In your
csprojadd aWINDOWSconstant and conditional target on yourMacross.Windows.Debuggingreference:<PropertyGroup Condition="'$(OS)' == 'Windows_NT'"> <DefineConstants>WINDOWS</DefineConstants> </PropertyGroup> <ItemGroup> <PackageReference Include="Macross.Windows.Debugging" Version="1.3.1" Condition="$(OS) == 'Windows_NT'" /> </ItemGroup>
-
In your bootstrap add a call to
ConfigureDebugWindowusingWINDOWS&DEBUGconditions:internal class Program { public static void Main(string[] args) => CreateHostBuilder(args).Build().Run(); public static IHostBuilder CreateHostBuilder(string[] args) { return Host .CreateDefaultBuilder(args) #if WINDOWS && DEBUG .ConfigureDebugWindow() #endif .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>()); } }
When you start debugging a web application Visual Studio will attach to an IIS
Express process by default. In that scenario the DebugWindow won't spawn until
a request comes through the web server and the process is actually spun up. You
will get a much better experience if you launch your code directly using the
compiled EXE. The recommended approach is to switch the default order in
.\Properties\launchSettings.json:
{
...
"profiles": {
"WebApplication1": {
"commandName": "Project",
"launchBrowser": false,
"launchUrl": "weatherforecast",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
...
}There are different ways to configure the DebugWindowLoggerOptions object.
-
At runtime:
The
ConfigureDebugWindowmethod accepts a few callbacks that can be used for configuration at runtime:.ConfigureDebugWindow( options => options.WindowTitle = "My Application DebugWindow Title", (window) => window.BackColor = Color.Red, (tab) => tab.BackColor = Color.Blue);
The first callback allows direct configuration of the
DebugWindowLoggerOptionsobject. It exposes basic options and things likely to be configured.The second and third callbacks allow direct manipulation of the
DebugWindowand anyDebugWindowTabPagecontrols as they are created. Use these for advanced configuration of the UI such as adding controls. -
Via
IConfigurationpipeline (AppSettings, Command-line, Environment Variables, etc.):The
DebugWindowLoggerOptionsobject will bind to theDebugWindowlogging configuration section:{ "Logging": { "DebugWindow": { "ShowDebugWindow": true, "MinimizeToSystemTray": true, "LogLevel": { "Default": "Debug", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } } } }Most properties tie directly to the options object.
LogLevelis special and controls which messages in the logging pipeline will be mapped to theILoggerpassing messages to the UI. See: Logging in .NET Core and ASP.NET CoreYou can also impact these settings via the command-line:
Start-Process ` -FilePath "TestWebApplication.exe" ` -WorkingDirectory "C:\WorkingDirectory\" ` -ArgumentList "--environment Development --Logging:DebugWindow:ShowDebugWindow=true"
-
Groups:
Log messages in .NET Core are written into categories. Typically the category is the [Namespace].[ClassName] which can lead to a lot of tabs being opened to display messages. To make things more useful, messages can be grouped together.
-
Config-based Grouping
The
GroupOptionssub-section can be used to define groups by filters. The defaults look like this:{ "Logging": { "DebugWindow": { "GroupOptions": [ { "GroupName": "System", "CategoryNameFilters": ["System*"] }, { "GroupName": "Microsoft", "CategoryNameFilters": ["Microsoft*"] } ] } } }Notes: 1) You should use wildcards when defining filters. 2) Once you define one group option, the two default rules will no longer be applied.
-
Code-based Grouping
When coding you can define groups dynamically. This is where the debug tool starts to become more powerful because you can group related messages across objects together easily.
using IDisposable Group = _Logger.BeginGroup("Business Logic Area"); _Logger.LogInformation("Starting logical process."); await ExecuteProcess().ConfigureAwait(false); _Logger.LogInformation("Logical process complete.");
In the above example everything that happens under the "Group" scope will be grouped together.
BeginGroup is a helper extension over
ILogger.BeginScope. This is the same thing:using IDisposable Scope = _Logger.BeginScope(new LoggerGroup("Business Logic Area")); _Logger.LogInformation("Starting logical process."); await ExecuteProcess().ConfigureAwait(false); _Logger.LogInformation("Logical process complete.");
-
If multiple groups are found for a log message than the last one applied will be
selected. To customize this behavior a Priority parameter is available, the
highest priority group will always be selected over lower priority grouping.
For more information on BeginGroup see
Macross.Logging.Abstractions.
The TestWebApplication has example middleware you can use to group messages by
Controller which is a really useful feature when debugging services.
See: ControllerNameLoggerGroupMiddleware.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseRouting(); // <- Order is important!
app.UseMiddleware<ControllerNameLoggerGroupMiddleware>();
...
}
public class ControllerNameLoggerGroupMiddleware
{
private readonly ILogger<ControllerNameLoggerGroupMiddleware> _Logger;
private readonly RequestDelegate _Next;
public ControllerNameLoggerGroupMiddleware(ILogger<ControllerNameLoggerGroupMiddleware> logger, RequestDelegate next)
{
_Logger = logger ?? throw new ArgumentNullException(nameof(logger));
_Next = next ?? throw new ArgumentNullException(nameof(next));
}
public async Task InvokeAsync(HttpContext context)
{
RouteValueDictionary? RouteValues = context?.Request.RouteValues;
IDisposable? Group = null;
if (RouteValues != null && RouteValues.TryGetValue("controller", out object ControllerName))
Group = _Logger.BeginGroup(ControllerName.ToString());
try
{
await _Next(context).ConfigureAwait(false);
}
finally
{
Group?.Dispose();
}
}
}Note: It is important that UseRouting be executed before this middleware
otherwise routing information won't be available.
- An IHostedService is registered into the Appication's IHostBuilder which manages a low-priority Thread hosting the UI.
- An ILoggerProvider is registered with the .NET Core logging platform for passing messages to the UI.
If you find the flattened message JSON format displayed in the UI conveniant and want to write it out into actual log files, see Macross.Logging.Files. To write it out to Console (stdout), see Macross.Logging.StandardOutput.
A demo web application using the DebugWindow and Macross.Logging.Files is
available here.
