Using Loupe with WinForms in .NET 6 & 8

WinForms for the Future!

We’ve been using WinForms for over 20 years - and it’s still the desktop technology to beat on Windows. With the .NET team bringing WinForms support to .NET 6 and up, it’s not going away any time soon.

Loupe Desktop is an entirely WinForms application - and we’re actively updating it to work with .NET 8. Many of our customers are updating their WinForms applications as well. Fortunately, Loupe’s agents are there for you - but you’ll need to make a few tweaks to get everything to work together.

Swapping Loupe.Agent.Core for Gibraltar.Agent

The first change is to remove references to any Gibraltar.* assemblies (they’re .NET Framework only) and using their equivalent Loupe.Agent.* assemblies which are part of the open source Loupe.Agent.Core repository on GitHub.

You’ll want to add these NuGet packages:

  • Loupe.Agent.Core.Services: Adds the full Loupe.Agent.Core (which has a backwards-compatible API to Gibraltar.Agent) and related features to integrate with HostBuilder and the Services collection.
  • Loupe.Extensions.Logging: Adds support for Microsoft.Extensions.Logging, the newer logging API used pervasively in .NET.
  • Loupe.Agent.PerformanceCounters: Recommended to capture performance metrics as your application runs.

You’ll find that your existing calls to Gibraltar.Agent.Log will still compile thanks to the Loupe.Agent.Core assembly which has all the same methods with the same capabilities.

Initializing your Application

.NET 6/7/8 don’t provide a clean way to combine the application startup models used in .NET Core & modern .NET with the traditional approach used in WinForms. This is the first major hurdle we need to cross to use newer libraries the way they want to work along with our existing WinForms code investment.

Your WinForms application’s entry point probably looks something like this:

[STAThread]
private static async Task Main()
{
    // Enabling visual styles and other key startup calls are rolled up into this new
    // call to Initialize.
    ApplicationConfiguration.Initialize();

    // Now call Application Run which creates a windows message pump and displays your form.
    Application.Run(new MainForm());

    // Once Application.Run exits, your application will exit too.
}

The critical part is that you create your main form and then invoke a function that blocks until your application is ready to exit (because that form is closing). This looks pretty similar to how modern .NET applications look where you setup your application and call Run() which blocks until your app is ready to exit. We need to unify these two approaches.

To do this, we do the setup of the modern .NET infrastructure the normal way:

var host = Host.CreateDefaultBuilder()
    .ConfigureServices((context, services) =>
    {
        // Initialize the Loupe Agent, including Performance Counters
        services.AddLoupe(builder => 
            builder.AddPerformanceCounters());

        // Add Loupe's support for Microsoft.Extensions.Logging.
        services.AddLoupeLogging();

        // Add your services here (optional).
    })
    .Build();

Then, instead of calling Run which would block we use the new Start method which will activate the host features (like background services) but return as soon as they’re set up. We can then start our WinForms app, as we normally would. To make sure we also call Stop, we wrap everything in a try/finally block:

try
{
    // Start our .NET host we built above.
    await host.StartAsync(); 

    // Do our normal WinForms application entry sequence
    ApplicationConfiguration.Initialize();
    Application.Run(new MainForm(logger));
}
finally
{
    await host.StopAsync();
}

Bringing it all together

We’ve published a sample at Github:Loupe.Agent.Core which bring it all together, like this:

internal static class Program
{
    private static IServiceProvider _serviceProvider;

    /// <summary>
    ///  The main entry point for the application.
    /// </summary>
    [STAThread]
    private static async Task Main()
    {
        var host = Host.CreateDefaultBuilder()
            .ConfigureServices((context, services) =>
            {
                services.AddLoupe(builder => 
                    builder.AddPerformanceCounters());

                services.AddLoupeLogging();

                // Add your services here (optional).
            })
            .Build();

        _serviceProvider = host.Services;

        try
        {
            await host.StartAsync(); 

            ApplicationConfiguration.Initialize();
            Application.Run(new MainForm());
        }
        catch (Exception ex)
        {
            logger.LogError(ex, 
                "Application exiting due to unhandled {Exception}", 
                ex.GetBaseException().GetType().Name);
            Log.EndSession(SessionStatus.Crashed, 
                "Application failed due to unhandled exception");
            await Log.SendSessions(SessionCriteria.ActiveSession);
        }
        finally
        {
            await host.StopAsync();
        }
    }
}

The only additional change in this example is adding a catch for unhandled exceptions coming out of MainForm so they’re recorded to the log and the session is marked as crashed. Also, we then push the session immediately to your Loupe Server repository on crash exit so you can triage the data right away.

Why Use the HostBuilder?

The Loupe.Agent.Core family of agents is designed to work with .NET Core / .NET 5+ which have a built in Dependency Injection system and configuration manager. By setting up the host builder like we do above and starting the host, that ensures Loupe gets its configuration data from the normal appsettings.json & environment variables. It also connects the agent into the application lifecycle so it knows when the application is starting and exiting. Without it, the application may not exit cleanly.

Going Farther with .NET 6

Modern .NET applications want to create just about everything from the host once it’s built so that dependencies can be automatically injected with the right scope.

To see how you can modify your WinForms application to take advantage of this pattern, check out part two of our WinForms series (Coming soon!)

Rock solid centralized logging

Unlimited applications, unlimited errors, scalable from solo startup to enterprise.