Click here to Skip to main content
15,879,348 members
Articles / Web Development / ASP.NET / ASP.NET Core

2 Ways to Log the Current User in ASP.NET Core

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
2 Jul 2021CPOL3 min read 7.4K   6   1
How to automatically add current user info to logs in ASP.NET Core

First shown in ASP.NET Boilerplate with log4net. Next shown in ABP Framework with the Microsoft Logging Framework and Serilog.

Image 1

If you've ever had to debug a problem with a production system, you know that good logs are absolutely essential. However, knowing what information might be useful, and how much is too much is almost an artform.

I had the opportunity to develop that artform recently when I published an ASP.NET Boilerplate based system into production. Overall, the deployment went well, but I realized that I'd failed to include the currently logged in user or tenant in logs. That made debugging issues harder.

Therefore, this is the story of how to add information about the currently logged in user, potentially including the tenant in a multi-tenant solution, in ASP.NET Core. First I'll show it in ASP.NET Boilerplate with log4net. Next, I'll show how with ABP Framework and the Microsoft Logging Framework via Serilog. Hopefully, you can find something here to adapt for your tech stack, and help you develop your inner log artistry.

ASP.NET Boilerplate + log4net

For log4net, the first trick is adding custom fields. That happens in the log4net.config configuration file. It turns out to be somewhat unintuitive via log4net's property{name} syntax:

XML
<appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender" >
    ...
    <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%-5level %date [%-5.5property{userid}] 
         [%-5.5property{tenantid}] [%-5.5thread] %-40.40logger - %message%newline" />
</layout>
</appender>

I surrounded the userid and tenantid fields with brackets and used the fixed-width pattern layout -5.5 to pad integer values less than 5 characters to 5 characters.

To populate those fields in log4net, you need set the property on a context, which makes it available for all logs. There are four contexts to choose from, but the one that makes the most sense here is the logical thread context, since that is the level at which requests are processed and it can persist across await points even if a different thread is used to resume the request. The code then looks like LogicalThreadContext.Properties["userid"] = ??. But where to set it.

The most appropriate place is in a middleware component during the request pipeline, right after authentication so that the current user is available. In other words:

C#
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
    ...
    app.UseAuthentication();
    
    app.UseSessionLogging();
    
    ...
}

UseSessionLogging is an extension method that looks like this:

C#
public static class SessionLoggingMiddlewareUtil
{
    public static void UseSessionLogging(this IApplicationBuilder app)
    {
        app.UseMiddleware<SessionLoggingMiddleware>();
    }
}

I chose Factory-based middleware activation in ASP.NET Core so that I could get dependency injection in order to get access to an IAbpSession, upon which I could get the current user and tenant. So the last piece of the puzzle is then:

C#
public class SessionLoggingMiddleware : IMiddleware, ITransientDependency
{
    private readonly IAbpSession _session;

    public SessionLoggingMiddleware(IAbpSession session)
    {
        _session = session;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        LogicalThreadContext.Properties["userid"] = _session.UserId;
        LogicalThreadContext.Properties["tenantid"] = _session.TenantId;
        await next(context);
    }
}

The full code is available in LeesStore PR #30.

Note that using other appenders like an ApplicationInsightsAppender are similar with some minor variation.

XML
<appender name="AiAppender" 
 type="Microsoft.ApplicationInsights.Log4NetAppender.ApplicationInsightsAppender, 
 Microsoft.ApplicationInsights.Log4NetAppender">
  <threshold value="Info" />
  <layout type="log4net.Layout.PatternLayout">
    <conversionPattern 
     value="%-5level %property{userid} %property{tenantid} %message%newline"/>
  </layout>
</appender>

The AdoNetAppender is slightly trickier:

XML
<appender name="AdoNetAppender" 
 type="MicroKnights.Logging.AdoNetAppender, MicroKnights.Log4NetAdoNetAppender">
  ..
  <commandText value="INSERT INTO LogEntries 
  ([Date],[Level],[Logger],[Message],[Exception],[UserId],[TenantId]) 
  VALUES (@log_date, @log_level, @logger, @message, @exception, @userid, @tenantid)" />
    ...
    <parameter>
      <parameterName value="@userid" />
      <dbType value="Int32" />
      <layout type="log4net.Layout.RawPropertyLayout">
        <key value="auserid" />
      </layout>
    </parameter>
</appender>

ABP Framework + Serilog

I love log4net. I've been using it forever. But serilog is more modern and feels more elegant. Using its equivalent of custom fields for a "console appender" is as simple as placing them in curly brackets in an output template set up in Program.cs:

C#
Log.Logger = new LoggerConfiguration()
    .Enrich.FromLogContext()
    .WriteTo.Async(c => c.File("Logs/logs.txt", outputTemplate:
        "{Level:u4} [{UserId}] [{TenantId}] [{Timestamp:HH:mm:ss}] 
         {Message:lj}{NewLine}{Exception}"))
    .WriteTo.Async(c => c.Console())
    .CreateLogger();

Setting those custom fields is done with LogContext.PushProperty(). Where to place that code is a bit trickier. I'm still a fan of a custom middleware component, but plugging in a middleware component in ABP Framework doesn't happen in Startup.cs. That's because ABP Framework decentralizes that logic, allowing each dependent Module the ability to register custom middleware.

However, there's no need for a custom module. Just add the middleware component like before but in the OnApplicationInitialization() method of the [MyProject]ApiHostModule.

C#
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
    ...
    app.UseAuthentication();
    app.UseSessionLogging();
}

Then the middleware component itself is extremely similar to the last one:

C#
public class SessionLoggingMiddleware : IMiddleware, ITransientDependency
{
    private readonly ICurrentUser _currentUser;
    private readonly ICurrentTenant _currentTenant;

    public SessionLoggingMiddleware(ICurrentUser currentUser, ICurrentTenant currentTenant)
    {
        _currentUser = currentUser;
        _currentTenant = currentTenant;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        using (Serilog.Context.LogContext.PushProperty("UserId", _currentUser.Id))
        using (Serilog.Context.LogContext.PushProperty("TenantId", _currentTenant.Id))
        {
            await next(context);
        }
    }
}

And now the console logs contain the user and tenant.

Image 2

Or perhaps you're sending them to Application Insights. Or if you're logging them to the database, you can expose them on an internal page:

Image 3

The full changes are in LeesStore PR 31.

Conclusion

This concludes two fairly different approaches to logging. Hopefully you can adapt something you learned here to avoid some production pain, or just to develop your artistic sensibilities.

This article was originally posted at http://www.leerichardson.com/feeds/posts/default

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United States United States
Lee is a Microsoft MVP and a prolific writer, speaker, and youtuber on .Net and open source topics. Lee is a Solution Samurai at InfernoRed ( http://infernoredtech.com). When not coding he enjoys running, mountain biking, smoking brisket, electronics, 3D printing, and woodworking. He is active on twitter where you can reach him @lprichar ( https://twitter.com/lprichar).

Comments and Discussions

 
Questioncode solution/project Pin
mag1314-Jul-21 20:22
mag1314-Jul-21 20:22 
code for test ?
magsoft

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.