Tracking Application Users in .NET Core with Loupe

One of the major new features in Loupe 4 was Application User Tracking - the ability to associate each log message and event with not just a user name but a complete user entity, including whatever additional information your application tracked about them. This enabled a number of new support scenarios - you could look for a user by name or organization in Loupe and find their record, then see details on any errors they had encountered, what versions they had used and how recently they ran them.

This capability all depended on finding the current user when a log message was record from Thread.CurrentPrincipal. Unfortunately, with .NET Core this property is basically useless: It is no longer updated by web frameworks and other authentication components, primarily because it couldn’t be maintained accurately as execution traveled across threads with async/await. Instead, the issue was left unaddressed in .NET Core itself and an alternate way of finding the current user was added to ASP.NET Core. This new approach wasn’t designed with middleware (like Loupe) in mind, only simple applications that could explicitly request the current user from the Controller base class which required us to step back and reassess how to provide this capability.

To address this gap, Loupe uses a two stage process:

  1. Identify the current IPrincipal: Since Thread.CurrentPrincipal is no longer set we need an alternate way to resolve the principal within the current application, be it an ASP.NET application or other. This is done using an IPrincipalResolver.
  2. Map the IPrincipal to an Application User: As before, we need to efficiently take the IPrincipal and let the application map data from it (and potentially other sources) to the Loupe Application User entity to provide the richest representation. This is done using an IApplicationUserProvider.

The Simplest Approach: Just Add ASP.NET Diagnostics

To make this as simple as possible, Loupe provides a few default implementations of the two interfaces it needs and automatically adds them when you add our ASP.NET Core Diagnostics:

public void ConfigureServices(IServiceCollection services)
{
    //Add Loupe and connect it to ASP.NET Core.
    services.AddLoupe()
        .AddAspNetCoreDiagnostics() //From Loupe.Agent.AspNetCore

    //add Loupe as a logger as well
    services.AddLogging(builder =>
    {
        builder.AddLoupe(); //from Loupe.Extensions.Logging
    });

    //and here goes the rest of your configuration
}

Under the covers, AddAspNetCoreDiagnostics automatically registers the two default implementations we want:

public static ILoupeAgentBuilder AddAspNetCoreDiagnostics(this ILoupeAgentBuilder builder) =>
    builder.AddListener<ActionDiagnosticListener>()
        .AddPrincipalResolver<ClaimsPrincipalResolver>()
        .AddApplicationUserProvider<ClaimsPrincipalApplicationUserProvider>();

The ClaimsPrincipalResolver assumes the current application is using the .NET Standard ClaimsPrincipal and gets the current one using the IHttpContextAccessor interface provided by ASP.NET, which is injected when the resolver is created automatically by the ASP.NET Service Host.

Unfortunately, there isn’t much user data we can easily resolve using this approach so the Application User will be quite sparse. If your application has more information available about the user then you’ll want to provide a customized ApplicationUserProvider. If all the information you want is associated with your IIdentity implementation, you can just provide a simple mapping function to AddAspNetCoreDiagnostics, like this:

public void ConfigureServices(IServiceCollection services)
{
    services.AddLoupe()
        .AddAspNetCoreDiagnostics(((principal, appUserFactory) =>
        {
            var appIdentity = pricipal.Identity as YourIdentityUser;

            if (appIdentity != null)
            {
                var user = appUserFactory.Value;
                user.Caption = appIdentity.DisplayName;
                user.EmailAddress = appIdentity.EmailAddress;
                return true;
            }

            return false;
        }));

    //...
}

This example presumes you created a custom identity type called YourIdentityUser which has DisplayName and EmailAddress properties. Your actual identity user would vary depending on your application design.

Creating Custom Implementations for More Elaborate Scenarios

There are a range of scenarios where you either have an entirely different way of determining who the current IPrincipal is and/or need access to more context to map it to an application user. To handle these scenarios, just create your own implementations of IPrincipalResolver or IApplicationUserProvider as needed and then map them explicitly to Loupe in the Loupe Builder, like this:

public void ConfigureServices(IServiceCollection services)
{
    //Add Loupe and connect it to ASP.NET Core & Our Providers.
    services.AddLoupe()
        .AddAspNetCoreDiagnostics()
        .AddPrincipalResolver<YourOptionalPrincipalResolver>()
        .AddApplicationUserProvider<YourOptionalApplicationUserProvider>();

    //...
}

The most likely scenario is that you need to directly work with other resources - like a database provider - to fully populate the application user and want them injected at startup. Here’s an example IApplicationUserProvider that takes a DBContext and then can use it to query for extended user data:

public class EFApplicationUserProvider : IApplicationUserProvider
{
    private YourDatabaseContext _databaseContext;

    public EFApplicationUserProvider(YourDatabaseContext databaseContext)
    {
        _databaseContext = databaseContext;
    }

    public bool TryGetApplicationUser(IPrincipal principal, Lazy<IApplicationUser> applicationUser)
    {
        //query for the user's profile data
        var userProfile = _databaseContext.UserProfiles.FirstOrDefault(u => u.Name = principal.Identity.Name)

        //if we got a user profile then go ahead and map its data to the Loupe ApplicationUser.
        if (userProfile != null)
        {
            var newUser = applicationUser.Value; //lazily creates the user.
            newUser.Caption = userProfile.DisplayName;
            newUser.Organization = userProfile.Company;
            newUser.EmailAddress = userProfile.EmailAddress;
            newUser.Properties.Add("License Key", userProfile.LicenseKey);
            //... Set any other properties you want.

            return true; //tells Loupe this is the best info for this user.  Loupe won't ask again.
        }

        return false; //by default tell Loupe no, we couldn't find that user.  It'll ask again.
    }
}

If you’re not sure entirely how to do this, no problem - check out our documentation that expands on this and if in doubt, make use of our exceptional customer support to steer you in the right direction.

Worth It

Having rich information available about your application users pays off handsomely when using Loupe Server. You can search for a user by any part of their information and then see all manner of details about them:

Server User Details

Application user information is integrated into both the web UI and Loupe Desktop, as well as exposed via our REST API so you can integrate it with your favorite customer support system.

Rock solid centralized logging

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