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:
- Log files appear -- Look in
${basedir}/logs/(the project output directory by default) - Console output appears -- You should see NLog-formatted output, not the default ASP.NET Core format
- Framework logs are silenced -- The
Microsoft.*andSystem.Net.Http.*rules should reduce noise significantly - Internal log is clean -- Check
internal-nlog.txtfor 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.

