Practical Logging Patterns for Reliable .NET Applications
Good logging is one of the most underrated tools in software development.
When done right, logs explain what your application is doing — even when things go wrong.
Logging is not just about writing messages to a file or console.
It’s about choosing what to log and how to log it safely and clearly.
Common pitfalls include:
-
Accessing properties on objects that may be
null -
Logging complex data structures without readable output
-
Producing logs that are too verbose or too vague
-
Creating unnecessary data just for logging purposes
This collection focuses on practical, everyday logging patterns:
-
Writing null-safe log statements
-
Turning collections into human-readable output
-
Logging only the information that matters
-
Choosing simple and efficient data structures for log data
Each example is intentionally small and generic, so the ideas can be reused in any .NET project.
Value
These patterns help you create logs that are stable, readable, and genuinely useful — especially when debugging production issues.
When data is loaded from a list or a database, there is always a chance that nothing is found. If the code still tries to access properties on a missing object, the log statement itself can crash the application.
A simple null check makes the behavior explicit and keeps the log stable:
var item = items.FirstOrDefault(x => x.Id == id);
if (item == null)
{
logger.LogWarning("No item found for Id={Id}", id);
}
else
{
logger.LogInformation(
"Id={Id}, Type={Type}",
item.Id,
item.Type);
}
This version clearly separates the “not found” case from the normal case and produces meaningful log messages for both situations.
When a more compact style is preferred, null operators can be used instead:
logger.LogInformation(
"Id={Id}, Type={Type}",
item?.Id ?? "<unknown>",
item?.Type ?? "<unknown>");
Both approaches prevent runtime errors and ensure that logging remains reliable even when data is incomplete.
Complex objects and dictionaries often contain far more data than a log entry really needs. Logging them directly can flood the log with noise or produce unreadable output.
A better approach is to extract only the most relevant values and create a compact summary:
var summary = dataMap.Select(x => new
{
Key = x.Key,
Count = x.Value.Count,
Status = x.Value.Status
});
logger.LogInformation("DataSummary={Summary}", summary);
This keeps the log focused on what matters: identifiers, counts, and simple status values. The result is easier to scan, easier to search, and more useful during debugging or monitoring.
When collections are logged directly, many logging systems only display the type name instead of the actual values. This makes it hard to understand what data was processed.
Converting the collection into a readable string solves this problem:
string[] values = { "One", "Two", "Three" };
var text = string.Join(", ", values);
logger.LogInformation("Values=[{Values}]", text);
To give additional context, the number of elements can also be logged:
logger.LogInformation(
"Count={Count}, Values=[{Values}]",
values.Length,
string.Join(", ", values));
Readable output improves troubleshooting and reduces the need to reproduce issues locally.
The choice between arrays and lists communicates intent and affects performance and safety.
When data is only needed for display or logging, an array represents a fixed snapshot and avoids accidental changes:
string[] names =
source.Select(x => x.Name).ToArray();
logger.LogInformation(
"Names=[{Names}]",
string.Join(", ", names));
Lists are useful when the collection must be modified or extended later:
var names =
source.Select(x => x.Name).ToList();
names.Add("NewItem");
Choosing the right type makes the code easier to understand and prevents unintended side effects.
Sometimes data is created only to be logged and never used again. Creating intermediate lists or arrays in those cases increases memory usage and adds unnecessary complexity.
A summary string can often be built directly from the source data:
var summary =
string.Join(", ",
source.Select(x => x.Name));
logger.LogInformation("Items=[{Items}]", summary);
If the source might be null, a safe fallback avoids runtime errors:
var safeSource = source ?? Enumerable.Empty<Item>();
var summary =
string.Join(", ",
safeSource.Select(x => x.Name));
logger.LogInformation("Items=[{Items}]", summary);
This approach keeps the code lean while still producing clear, useful log output.
Comments