Lightweight, framework-agnostic logging helpers on top of Microsoft.Extensions.Logging.
NoNameLogger does not replace your logging provider (Serilog, Console, Seq, Application Insights, etc.). Instead, it provides:
- A fluent API for building structured log entries.
- A reusable, immutable logging context abstraction.
- Centralized, strongly-typed log events and message templates (generic, not domain-specific).
- Simple helpers for timed operations and common patterns.
This makes your logging:
- Easier to read and write.
- More consistent across projects.
- Safer to refactor (events/messages live in one place).
- 1. Project Structure
- 2. Requirements
- 3. Getting Started
- 4. Architecture
- 5. Extending the Framework
- 6. Integration with Serilog
- 7. Best Practices
- 8. Troubleshooting
- 9. Contributing
- 10. Summary
The library is organized into a few small namespaces and files:
src/NoNameLogger
├─ Abstractions
│ └─ ILoggingContext.cs
│
├─ Context
│ ├─ LoggingContext.cs
│ ├─ LoggingContextBuilder.cs
│ ├─ LoggingContextKeys.cs
│ ├─ LoggingScope.cs
│ └─ LoggingScopeExtensions.cs
│
├─ Core
│ ├─ LogEntryBuilder.cs
│ ├─ LoggerExtensions.cs
│ └─ TimedLogOperation.cs
│
├─ Conventions
│ ├─ CommonLogEvents.cs
│ └─ CommonLogMessages.cs
│
└─ Enricher
├─ ConsolePropertyFilterEnricher.cs
└─ LoggingScopeEnricher.cs
samples/NoNameLogger.Demo
└─ Demonstrates basic usage patterns (console app)
Note: Demo and runnable samples live under
samples/to keep the NuGet package clean and domain-agnostic.
ILoggingContext- Immutable key/value bag that can be attached to any log entry.
- Exposes
IReadOnlyDictionary<string, object?> PropertiesandTryGet.
-
LoggingContext- Concrete implementation of
ILoggingContext. - Stores data internally in a
FrozenDictionary<string, object?>for performance and thread-safety.
- Concrete implementation of
-
LoggingContextBuilder- Fluent builder for constructing
LoggingContextinstances. - You can add arbitrary keys via
With(key, value)/WithMany(...). - Includes convenience methods for common keys:
WithEnvironment,WithApplication,WithService,WithMachineNameWithUserId,WithUserName,WithTenantWithCorrelationId,WithRequestId,WithSessionIdWithOperation,WithOperationIdWithHttpMethod,WithPath,WithRoute,WithStatusCode,WithClientIp
- Fluent builder for constructing
-
LoggingContextKeys- String constants for the well-known property names above.
- Helps keep keys consistent across a codebase.
-
LoggingScope- Provides ambient (async-local) storage for
ILoggingContext. - Establish a context at the entry point of a request/job so all logs in that async flow can use it.
- Key members:
Current– Gets the current ambient context (ornullif none).HasCurrent– Returnstrueif an ambient context is active.Begin(ILoggingContext context)– Starts a new scope; returnsIDisposablethat restores the previous context on dispose.BeginWithProperties(...)– Starts a nested scope by adding properties to the current context.GetEffectiveContext(ILoggingContext? explicitContext)– Merges ambient and explicit contexts.
- Provides ambient (async-local) storage for
-
LoggingScopeExtensions- Helper extension methods for working with ambient contexts:
WithProperties(...)– Creates a new context by cloning and adding properties.MergeWith(...)– Merges two contexts.BuilderFromCurrent()– Gets a builder initialized from the current ambient context.PushProperties(...)– Starts a nested scope with additional properties.
- Helper extension methods for working with ambient contexts:
-
LogEntryBuilder- Fluent builder for constructing a structured log entry and writing it to
ILogger. - Lets you set:
- Level:
Trace,Debug,Information,Warning,Error,Critical. EventIdvia.Event(EventId)or.Event(int id, string? name = null).- Message template and arguments via
.Message("...", args...). - Exception via
.Exception(ex). - Context via
.WithContext(ILoggingContext). - Additional properties via
.WithProperty(key, value)/.WithProperties(...).
- Level:
- Call
.Write()at the end to actually log. - Internally builds a
Dictionary<string, object?>state containing:- Context properties.
- Custom properties.
- The message template under
{OriginalFormat}so providers can do structured logging.
- Fluent builder for constructing a structured log entry and writing it to
-
LoggerExtensions- Adds extensions on top of
ILogger:Log(this ILogger logger)→ starts a fluentLogEntryBuilder.Log(this ILogger logger, ILoggingContext context)→ same, but with a context attached.- Shortcut methods:
LogTrace,LogDebug,LogInformation,LogWarning,LogError,LogCritical.- Each takes:
EventId, message template, optionalILoggingContext, optionalException, optional extra properties, andparams args.
- Adds extensions on top of
-
TimedLogOperation- Small helper that measures operation duration and logs start/completion/failure.
-
CommonLogEvents- Generic
EventIdsets grouped by category:System,Http,Database,Operation,Cache,Security,Business
- You can use them as-is or define your own equivalents.
- Generic
-
CommonLogMessages-
Generic message templates matching the events above.
-
Example:
public static class Operation { public const string Started = "Operation {OperationName} started"; public const string Completed = "Operation {OperationName} completed in {ElapsedMs} ms"; public const string Failed = "Operation {OperationName} failed in {ElapsedMs} ms. Error: {ErrorMessage}"; }
-
-
LoggingScopeEnricher- Serilog enricher that automatically adds properties from
LoggingScope.Currentto log events. - Bridges ambient logging context with Serilog structured logging.
- Serilog enricher that automatically adds properties from
-
ConsolePropertyFilterEnricher- Serilog enricher that filters out specified properties from console output.
Runnables live in samples/NoNameLogger.Demo.
- Demonstrates:
- explicit context + fluent logging
- ambient context via
LoggingScope - timed operations
- error handling patterns
- .NET: .NET 6.0 or later
- Dependency:
Microsoft.Extensions.Logging.Abstractions - Providers: Works with any provider that integrates with
ILogger(Serilog, NLog, Seq, Application Insights, etc.)
NoNameLogger extends ILogger without replacing your existing logging infrastructure.
Install the NuGet package:
dotnet add package NoNameLoggerRequired namespaces:
using Microsoft.Extensions.Logging;
using NoNameLogger.Application.Logging.Abstractions;
using NoNameLogger.Application.Logging.Context;
using NoNameLogger.Application.Logging.Core;
using NoNameLogger.Application.Logging.Conventions;public class SampleService
{
private readonly ILogger<SampleService> _logger;
public SampleService(ILogger<SampleService> logger)
{
_logger = logger;
}
public void DoWork()
{
var context = LoggingContextBuilder
.Create()
.WithEnvironment("Production")
.WithApplication("OrderApi")
.WithUserId("user-123")
.WithCorrelationId("corr-456")
.Build();
_logger.Log(context)
.Information()
.Event(CommonLogEvents.Http.RequestReceived)
.Message(CommonLogMessages.Http.RequestStarted)
.WithProperty("Path", "/api/orders")
.WithProperty("Method", "GET")
.Write();
}
}_logger.LogInformation(
CommonLogEvents.System.StartupCompleted,
CommonLogMessages.System.StartupCompleted,
context: context);You can attach extra properties directly:
_logger.LogError(
CommonLogEvents.Business.ValidationFailed,
CommonLogMessages.Business.ValidationFailed,
context: context,
properties: new []
{
new KeyValuePair<string, object?>("Entity", "Order"),
new KeyValuePair<string, object?>("Reason", "Missing customer id"),
});public async Task ProcessPaymentAsync(ILoggingContext context)
{
using var op = TimedLogOperation.Start(
_logger,
operationName: "ProcessPayment",
startEvent: CommonLogEvents.Operation.Started,
completedEvent: CommonLogEvents.Operation.Completed,
failedEvent: CommonLogEvents.Operation.Failed,
context: context);
try
{
await Task.Delay(250);
op.Complete();
}
catch (Exception ex)
{
op.Fail(ex);
throw;
}
}Instead of passing ILoggingContext through every method, establish an ambient context at the entry point of a request/job.
public async Task<IActionResult> GetOrderAsync(string orderId)
{
var context = LoggingContextBuilder
.Create()
.WithCorrelationId(Guid.NewGuid().ToString())
.WithUserId(User.Identity?.Name ?? "anonymous")
.WithOperation("GetOrder")
.With("OrderId", orderId)
.Build();
using (LoggingScope.Begin(context))
{
return await _orderService.GetOrderAsync(orderId);
}
}_logger.Log()
.Information()
.Event(CommonLogEvents.Operation.Started)
.Message("Fetching order {OrderId}", orderId)
.Write();using (LoggingScope.BeginWithProperties(("ProductId", productId)))
{
_logger.LogInformation(CommonLogEvents.Operation.Started, "Checking inventory", context: null);
}NoNameLogger builds a Dictionary<string, object?> as the logging state containing:
- Context properties
- Additional properties
- Message template under
{OriginalFormat}
Providers like Serilog recognize {OriginalFormat} and the state dictionary to produce structured logs.
LoggingContextis immutable and usesFrozenDictionaryfor thread-safety and performance- Builders are short-lived
- Minimal runtime overhead while providing a rich API
NoNameLogger is intentionally domain-agnostic. Common extension patterns:
public static class BookingContextBuilder
{
public static ILoggingContext CreateForBooking(string bookingId, string userId)
{
return LoggingContextBuilder
.Create()
.WithApplication("BookingApi")
.WithUserId(userId)
.With("BookingId", bookingId)
.Build();
}
}public static class BookingLogEvents
{
public static readonly EventId BookingCreated = new(8000, nameof(BookingCreated));
public static readonly EventId BookingCancelled = new(8001, nameof(BookingCancelled));
}
public static class BookingLogMessages
{
public const string BookingCreated = "Booking {BookingId} created for user {UserId}";
public const string BookingCancelled = "Booking {BookingId} cancelled";
}Create wrappers for repeated patterns (API endpoints, background jobs, etc.) that prefill context, events, or messages.
When using Serilog as the provider for ILogger, the state dictionary and {OriginalFormat} are recognized automatically.
_logger.Log(context)
.Information()
.Event(CommonLogEvents.Http.RequestCompleted)
.Message(CommonLogMessages.Http.RequestCompleted)
.WithProperty("Path", path)
.WithProperty("Method", method)
.WithProperty("ElapsedMs", elapsedMs)
.Write();To automatically include ambient context properties in all log events:
Log.Logger = new LoggerConfiguration()
.Enrich.With<LoggingScopeEnricher>()
.Enrich.With<ConsolePropertyFilterEnricher>()
.WriteTo.Console()
.CreateLogger();- Keep context keys consistent (use
LoggingContextKeyswhere possible) - Centralize events and message templates (avoid scattering
EventIdand templates) - Add properties intentionally (prioritize diagnostics value)
- Wrap critical flows in
TimedLogOperation - Keep domain-specific helpers in application projects, not in the core library
- Ensure you pass context:
logger.Log(context)...orLogInformation(..., context: context, ...) - If using ambient context, ensure
LoggingScope.Begin(context)is active - For Serilog, use
LoggingScopeEnricherto enrich ambient context properties
- Ensure
.Message(...)is called before.Write()
Contributions are welcome.
Use Conventional Commits:
<type>(<scope>): <description>
Examples:
feat(enricher): add LoggingScopeEnricher for automatic context enrichment
fix(context): handle null context in GetEffectiveContext
refactor(core): extract state building logic to separate method
chore(docs): update README with demo usage
NoNameLogger enhances Microsoft.Extensions.Logging by providing:
- Fluent structured logging via
LogEntryBuilder - Immutable
ILoggingContextfor consistent properties - Centralized
EventIdsets and message templates - Ambient context support via
LoggingScope - Optional Serilog enrichers for automatic context enrichment
Drop it into any .NET application using ILogger to improve logging structure, consistency, and maintainability.