BrandGhost
Getting Started with NLog in ASP.NET Core

Getting Started with NLog in ASP.NET Core

Setting up NLog in ASP.NET Core is straightforward, but there are enough configuration choices and startup-pattern details to trip up even experienced developers. Do you use an XML config file or put everything in appsettings.json? How do you handle log flushing on shutdown? What about the try/catch/finally pattern that the official NLog docs recommend?

This guide covers the complete setup from scratch, including both configuration styles, the proper startup/shutdown pattern for ASP.NET Core and Worker Services, and how to configure the most useful baseline options. By the end you'll have a production-ready NLog configuration you can drop into any .NET 8 project.

For broader context on .NET logging before going NLog-specific, Logging in .NET: The Complete Developer's Guide is worth reading first.

What You'll Need

  • .NET 8 SDK or later
  • An ASP.NET Core Web API, Blazor, or Worker Service project
  • NuGet access

No third-party log server is required for this guide -- we'll log to files and the console. Later articles in this series cover Seq, Elasticsearch, and custom targets.

Installing NLog

For ASP.NET Core, one package gets you everything:

dotnet add package NLog.Web.AspNetCore

This pulls in the base NLog package as a dependency and adds:

  • The UseNLog() host builder extension
  • ASP.NET Core-aware layout renderers (${aspnet-request-url}, ${aspnet-mvc-action}, ${aspnet-TraceIdentifier}, etc.)
  • Integration with Microsoft.Extensions.Logging

For a Worker Service or console application that doesn't need HTTP-aware renderers:

dotnet add package NLog
dotnet add package NLog.Extensions.Logging

The Two Configuration Approaches

NLog supports two configuration styles. You can switch between them (or combine them) at any time without changing your application code.

Approach 1: nlog.config (XML)

Create a file named nlog.config in your project root:

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      throwConfigExceptions="true"
      internalLogLevel="Warn"
      internalLogFile="${basedir}/internal-nlog.txt">

  <targets>
    <!-- Async wrapper prevents logging from blocking request threads -->
    <target xsi:type="AsyncWrapper" name="asyncFile" queueLimit="5000" overflowAction="Discard">
      <target xsi:type="File"
              name="logfile"
              fileName="${basedir}/logs/app-${shortdate}.log"
              archiveFileName="${basedir}/logs/archives/app-{#}.log"
              archiveEvery="Day"
              archiveNumbering="Rolling"
              maxArchiveFiles="14"
              layout="${longdate}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />
    </target>

    <target xsi:type="Console"
            name="console"
            layout="${level:truncate=4:uppercase=true}|${logger:shortName=true}|${message} ${exception:format=message}" />
  </targets>

  <rules>
    <!-- Silence noisy framework loggers at Info and below; final stops further evaluation -->
    <logger name="Microsoft.*" maxlevel="Info" final="true" />
    <logger name="System.Net.Http.*" maxlevel="Info" final="true" />
    <!-- All other loggers: Debug and above -->
    <logger name="*" minlevel="Debug" writeTo="asyncFile,console" />
  </rules>
</nlog>

Register the file to copy to the output directory in your .csproj:

<ItemGroup>
  <Content Include="nlog.config">
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </Content>
</ItemGroup>

Key attributes on the root <nlog> element:

Attribute Purpose
autoReload="true" Hot-reload config changes without restarting the app
throwConfigExceptions="true" Throw on config errors instead of silently failing
internalLogLevel="Warn" NLog's own diagnostic log level
internalLogFile Where NLog logs its own messages (separate from app logs)

Always set throwConfigExceptions="true" during development. Silent configuration failures are difficult to diagnose -- you'll just wonder why no logs appear.

Approach 2: appsettings.json

If your team prefers centralizing all configuration in appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "NLog": {
    "autoReload": true,
    "throwConfigExceptions": true,
    "internalLogLevel": "Warn",
    "extensions": [
      { "assembly": "NLog.Web.AspNetCore" }
    ],
    "targets": {
      "async": true,
      "logfile": {
        "type": "File",
        "fileName": "${basedir}/logs/app-${shortdate}.log",
        "archiveFileName": "${basedir}/logs/archives/app-{#}.log",
        "archiveEvery": "Day",
        "archiveNumbering": "Rolling",
        "maxArchiveFiles": 14,
        "layout": "${longdate}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}"
      },
      "console": {
        "type": "Console",
        "layout": "${level:truncate=4:uppercase=true}|${logger:shortName=true}|${message}"
      }
    },
    "rules": [
      { "logger": "Microsoft.*", "maxLevel": "Info", "final": true },
      { "logger": "System.Net.Http.*", "maxLevel": "Info", "final": true },
      { "logger": "*", "minLevel": "Debug", "writeTo": "logfile,console" }
    ]
  }
}

Note "async": true in the targets block -- this is the appsettings.json equivalent of wrapping all targets in AsyncWrapper. It applies asynchronous writing to every target in the configuration with a single setting.

You can also create an appsettings.Development.json that overrides the minimum log level to Trace locally without changing production config:

{
  "NLog": {
    "rules": [
      { "logger": "Microsoft.*", "maxLevel": "Info", "final": true },
      { "logger": "*", "minLevel": "Trace", "writeTo": "logfile,console" }
    ]
  }
}

Program.cs Setup: ASP.NET Core (.NET 8)

XML config file approach

using NLog.Web;

// Get a logger for startup errors before DI is initialized
var logger = NLogBuilder
    .ConfigureNLog("nlog.config")
    .GetCurrentClassLogger();

try
{
    var builder = WebApplication.CreateBuilder(args);

    // Replace the default logging providers with NLog
    builder.Logging.ClearProviders();
    builder.Host.UseNLog();

    builder.Services.AddControllers();
    builder.Services.AddEndpointsApiExplorer();

    var app = builder.Build();

    app.UseHttpsRedirection();
    app.MapControllers();
    app.Run();
}
catch (Exception ex)
{
    // Log startup failures before the host starts
    logger.Fatal(ex, "Application startup failed");
    throw;
}
finally
{
    // Flush and close all targets. Closing a target flushes its internal buffer.
    NLog.LogManager.Shutdown();
}

appsettings.json config approach

When using the JSON configuration approach, NLog discovers its config from IConfiguration automatically:

using NLog.Web;

var builder = WebApplication.CreateBuilder(args);

builder.Logging.ClearProviders();
builder.Host.UseNLog();

builder.Services.AddControllers();

var app = builder.Build();
app.MapControllers();
app.Run();

With appsettings.json, the NLog section is picked up during host builder initialization -- no explicit ConfigureNLog call needed. If you want to capture pre-startup errors, add the outer try/catch/finally with a manually created logger the same way as the XML approach, but use NLog.LogManager.Setup().LoadConfigurationFromAppSettings().

Program.cs Setup: Worker Service

Worker Services use a slightly different host builder. NLog setup is the same conceptually:

using NLog.Web;

var builder = Host.CreateApplicationBuilder(args);

builder.Logging.ClearProviders();
builder.Logging.AddNLog();  // Use AddNLog() for non-web hosts

builder.Services.AddHostedService<OrderProcessingWorker>();

var host = builder.Build();

try
{
    host.Run();
}
finally
{
    NLog.LogManager.Shutdown();
}

For Worker Services, add NLog.Extensions.Logging and call AddNLog() on the logging builder rather than UseNLog() on the host. UseNLog() is specific to IHostBuilder web variants.

Injecting and Using ILogger

Once NLog is registered, inject ILogger<T> exactly as you would with any other provider:

public sealed class OrderController : ControllerBase
{
    private readonly ILogger<OrderController> _logger;
    private readonly IOrderService _orderService;

    public OrderController(
        ILogger<OrderController> logger,
        IOrderService orderService)
    {
        _logger = logger;
        _orderService = orderService;
    }

    [HttpPost("{orderId}/process")]
    public async Task<IActionResult> ProcessOrder(int orderId)
    {
        _logger.LogInformation("Received request to process order {OrderId}", orderId);

        try
        {
            await _orderService.ProcessAsync(orderId);
            _logger.LogInformation("Order {OrderId} processed successfully", orderId);
            return Ok();
        }
        catch (OrderNotFoundException ex)
        {
            _logger.LogWarning(ex, "Order {OrderId} not found", orderId);
            return NotFound();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unexpected error processing order {OrderId}", orderId);
            return StatusCode(500);
        }
    }
}

Use message template syntax -- {OrderId} rather than $"Order {orderId}". String interpolation collapses the structured property into the message string. Template syntax preserves OrderId as a separate named value, which structured log targets (Seq, Elasticsearch) can index and filter independently.

For a refresher on how DI wires ILogger<T> into your controllers and services, IServiceCollection in C# -- Complete Guide with AddSingleton, AddScoped & AddTransient walks through the fundamentals.

Verifying Your Setup

After running the application, check:

  1. Log files appear -- Look in ${basedir}/logs/ (the project output directory by default)
  2. Console output appears -- You should see NLog-formatted output, not the default ASP.NET Core format
  3. Framework logs are silenced -- The Microsoft.* and System.Net.Http.* rules should reduce noise significantly
  4. Internal log is clean -- Check internal-nlog.txt for NLog's own diagnostic messages; any errors there indicate config issues

If no logs appear, check internal-nlog.txt first. throwConfigExceptions="true" will surface errors on startup, but the internal log captures anything NLog itself logs after initialization.

Configuration Best Practices

A few things to set from day one:

Always set throwConfigExceptions="true" in development. Silent NLog config failures mean your application starts normally but nothing gets logged. Fail loudly during development.

Use autoReload="true" in all environments. This lets you adjust log levels and targets at runtime -- invaluable in production when you need to temporarily increase verbosity to diagnose an issue without a restart.

Always call NLog.LogManager.Shutdown() in finally. NLog uses AsyncWrapper internally. Without Shutdown(), buffered log entries may never reach their targets when the process exits.

Silence Microsoft framework logs explicitly. The Microsoft.* and System.Net.Http.* rules with final="true" prevent megabytes of EF Core query logs and HTTP client tracing from drowning out your application logs.

Put development-specific overrides in appsettings.Development.json. Don't lower the minimum log level in production config. Override it only for local development.

Comparison with Serilog Setup

If you've set up Serilog before, the structural differences are:

NLog Serilog
Config style XML file or JSON section C# fluent API in Program.cs
ASP.NET Core integration builder.Host.UseNLog() builder.Host.UseSerilog()
Shutdown LogManager.Shutdown() Log.CloseAndFlush()
Async logging AsyncWrapper target or async=true Async sinks vary by package

Both integrate identically with ILogger<T> from the application code perspective. How to Set Up Serilog in ASP.NET Core: Step-by-Step Guide shows the equivalent Serilog setup for direct comparison.

Frequently Asked Questions

What package do I need to use NLog in ASP.NET Core?

Install NLog.Web.AspNetCore from NuGet. This single package provides the UseNLog() host builder extension, the ${aspnet-*} layout renderers for HTTP request context, and the Microsoft.Extensions.Logging provider integration. The base NLog package is pulled in as a dependency automatically.

Should I use nlog.config or appsettings.json for NLog configuration?

Both work equally well. Use nlog.config (XML) if you want hot-reloadable configuration that's completely separate from application settings, or if your team is already familiar with XML-based NLog config. Use appsettings.json if you prefer centralizing all configuration in one file and want environment-specific overrides via appsettings.Development.json or environment variables.

Why do I need the try/catch/finally pattern in Program.cs?

The outer try/catch/finally pattern serves two purposes. First, the catch block lets you log fatal startup errors that occur before the DI container is initialized -- without this, startup exceptions may go unlogged. Second, NLog.LogManager.Shutdown() in finally ensures that any buffered log entries (from AsyncWrapper or buffering targets) are flushed to their targets before the process exits.

How do I stop NLog from logging noisy Microsoft framework messages?

Add rules with final="true" for Microsoft.* and System.Net.Http.* at the top of your rules list:

<logger name="Microsoft.*" maxlevel="Info" final="true" />
<logger name="System.Net.Http.*" maxlevel="Info" final="true" />

The final="true" attribute stops rule evaluation after matching. This silences EF Core SQL query logs, HTTP client request details, and other verbose framework output without affecting your application logs. Adjust maxlevel to Warn if you want to retain Warning-level messages from Microsoft namespaces.

Does NLog work with .NET 8 minimal APIs?

Yes. The setup is identical for minimal API projects -- builder.Logging.ClearProviders() and builder.Host.UseNLog() work the same way regardless of whether you're using controller-based or minimal API routing. The ILogger<T> you inject into your endpoint handlers or services is provided by NLog.

What is internal-nlog.txt used for?

internal-nlog.txt is NLog's self-diagnostics log. It captures messages from NLog itself -- configuration parse errors, target write failures, and other NLog-internal events. When your application logs aren't appearing as expected, internal-nlog.txt is the first place to look. Set internalLogLevel="Warn" in production to keep it quiet under normal operation.

How do I configure NLog differently for Development vs Production?

Use appsettings.Development.json to override the NLog section for local development. For example, lower the minimum log level from Info to Trace locally, or add a console target that isn't present in production. NLog's autoReload="true" also lets you edit nlog.config at runtime in any environment without restarting the application.

NLog in .NET: Complete Guide to Flexible Logging

Master NLog in .NET with this complete guide. Learn targets, rules, layout renderers, structured logging, and performance optimization for C# applications.

How to Set Up Serilog in ASP.NET Core: Step-by-Step Guide

Learn how to set up Serilog in ASP.NET Core with two-stage initialization, appsettings.json configuration, and structured request logging. .NET 9 and .NET 10.

Logging in .NET: The Complete Developer's Guide

Master logging in .NET with ILogger, structured logging, log levels, Serilog, and OpenTelemetry. Complete guide for .NET 9 and .NET 10 developers.

An error has occurred. This application may no longer respond until reloaded. Reload