CoreDesign.Logging 1.1.0

dotnet add package CoreDesign.Logging --version 1.1.0
                    
NuGet\Install-Package CoreDesign.Logging -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CoreDesign.Logging" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CoreDesign.Logging" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="CoreDesign.Logging" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CoreDesign.Logging --version 1.1.0
                    
#r "nuget: CoreDesign.Logging, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CoreDesign.Logging@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CoreDesign.Logging&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=CoreDesign.Logging&version=1.1.0
                    
Install as a Cake Tool

CoreDesign.Logging

CoreDesign.Logging provides compile-time logging decorators generated from a single attribute. Place [LoggingDecorator] on any interface and the included Roslyn source generator produces a decorator class that logs every method invocation, return value, and exception. Implementations stay free of log statements while still producing structured, consistent log output for every operation.

A DispatchProxy-based logging middleware is also included for cases where a proxy is preferred over a generated decorator.

Installation

dotnet add package CoreDesign.Logging

Mark an interface

Apply [LoggingDecorator] to any interface you want wrapped:

[LoggingDecorator]
public interface ICreateForecastHandler
{
    Task<OneOf<WeatherForecast, BadRequestMessage>> CreateAsync(Request request, Guid userId, CancellationToken ct);
}

The generator creates CreateForecastHandlerLoggingDecorator at compile time. No hand-written boilerplate, no reflection at runtime.

Generic interfaces are fully supported. The decorator class carries the same type parameters and constraint clauses as the interface:

[LoggingDecorator]
public interface IRepository<T> where T : class
{
    Task<T?> GetByIdAsync(Guid id, CancellationToken ct);
    Task SaveAsync(T entity, CancellationToken ct);
}

The generator produces RepositoryLoggingDecorator<T> : IRepository<T> where T : class. Registration in DecorateWithLogging() uses the open-generic form so all concrete registrations of the interface are covered by a single call.

Register

After marking all interfaces, call the generated extension method once during startup:

services.AddTransient<ICreateForecastHandler, CreateForecastHandler>();
// ... other registrations ...
services.DecorateWithLogging();

DecorateWithLogging() is generated alongside the decorators and wraps every marked interface in one call.

What gets logged

Situation Level
Method called Information (method name and each parameter)
Method returned a success result Information (method name and return value)
Method returned Task (no value) Information (method name and "completed")
Method returned a NotFoundMessage, BadRequestMessage, or other error type Warning (method name and return value)
Method threw an exception Error (exception and method name)

Both synchronous and Task/Task<T> methods are fully supported. For OneOf<T0, T1, ...> return types, each union arm is logged at the appropriate level: success arms at Information, arms whose type name contains NotFound, BadRequest, Error, Failure, Unauthorized, Forbidden, Conflict, or InvalidOperation at Warning.

Sensitive data

[Redact]

Apply [Redact] to any parameter that should not appear in logs. The generated decorator replaces that argument with "[REDACTED]" in the invocation log while passing the actual value to the implementation.

[LoggingDecorator]
public interface IAuthService
{
    Task<LoginResult> LoginAsync(string username, [Redact] string password);
}
[Suppress]

Apply [Suppress] to a method to skip all logging for it. The generated decorator passes the call straight through to the inner implementation with no log entries.

[LoggingDecorator]
public interface ITokenService
{
    [Suppress]
    Task<string> IssueTokenAsync(string userId);
}

Interface properties and indexers

Properties and indexers declared on the interface are implemented as pure pass-throughs in the generated decorator. No logging is emitted for property access — only ordinary methods are logged.

[LoggingDecorator]
public interface ISessionStore
{
    string this[string key] { get; set; }
    int Count { get; }
    Task<bool> ExistsAsync(string key, CancellationToken ct);
}

The generated decorator delegates this[key] and Count directly to _inner with no log entries. ExistsAsync is logged normally.

Controlling log output size

The proxy middleware's [TruncateLog] attribute is not supported by the generator. The equivalent control belongs in the structured logging configuration, where limits apply consistently to all sinks and can be adjusted per environment without a code change.

With Serilog, add a Destructure block to the Serilog section of appsettings.json:

"Serilog": {
  "Destructure": [
    { "Name": "ToMaximumDepth", "Args": { "maximumDestructuringDepth": 5 } },
    { "Name": "ToMaximumStringLength", "Args": { "maximumStringLength": 500 } },
    { "Name": "ToMaximumCollectionCount", "Args": { "maximumCollectionCount": 10 } }
  ]
}

ToMaximumDepth caps how many levels deep Serilog will traverse when destructuring an object. Without this setting, a deeply nested object graph — such as an EF Core entity with navigation properties — will cause Serilog to stream an enormous payload into every sink, which can hang the application. ToMaximumStringLength truncates any string value captured during destructuring. ToMaximumCollectionCount caps how many elements are captured from arrays and collections. All three settings are tunable per environment: raise limits in appsettings.Development.json to see full payloads while debugging, or tighten them in appsettings.Production.json under load.

AOT compatibility

Generated decorators are plain C# classes with direct method calls. There is no reflection, no DispatchProxy, and no runtime code generation. They are fully compatible with .NET Native AOT.


Proxy Middleware (Alternative)

LoggingMiddleware<T> wraps any class behind an interface using DispatchProxy and automatically logs every method invocation, return value, and exception. This approach requires no compile step, but uses runtime reflection and is not AOT compatible.

Register a single class with the proxy

Replace the standard AddTransient (or AddScoped) call with AddWithLogging:

services.AddWithLogging<IWeatherForecastService, WeatherForecastService>();

Automatic registration with ILoggable

Implement the ILoggable marker interface on any class to opt it into automatic logging registration:

public class CreateForecastHandler(...) : ICreateForecastHandler, ILoggable { ... }
public class GetForecastHandler(...) : IGetForecastHandler, ILoggable { ... }

Then register all marked classes in a single call:

services.AddWithLogging(typeof(Program).Assembly);

Proxy-specific attributes

In addition to [Redact] and [Suppress], the proxy middleware supports:

[TruncateLog]

Return values are serialized to JSON and truncated at 500 characters by default. Apply [TruncateLog] to override the limit for a specific method:

[TruncateLog(2000)]
Task<IReadOnlyList<WeatherForecast>> GetAllAsync(CancellationToken ct);

[TruncateLog(0)]   // disables truncation entirely
Task<ServiceStatus> GetStatusAsync();

The log suffix reflects the reason for any truncation:

Suffix Meaning
... [truncated, total N chars] Output exceeded the length limit
... [depth limit reached] Object nesting exceeded the internal depth cap
... [truncated, depth limit reached] Both limits were hit

Serialization is cycle-safe. Circular references are written as null rather than throwing.

Proxy lifetime

Both overloads default to Transient. Pass a different lifetime when needed:

services.AddWithLogging<IMyService, MyService>(ServiceLifetime.Scoped);
services.AddWithLogging(typeof(Program).Assembly, ServiceLifetime.Scoped);

Choosing an approach

| Concern | Source Generator | DispatchProxy | |---|---|---| | Boilerplate | None (one attribute) | None (ILoggable or one-line registration) | | Async handling | Direct await | Runtime reflection via MakeGenericMethod | | AOT compatibility | Yes | No | | Performance | Direct method calls | MethodInfo.Invoke on every call | | Compile-time safety | Compiler enforces interface completeness | Interface changes are silent | | Log output size control | Serilog Destructure config (per-environment, no rebuild) | [TruncateLog] attribute (per-method, requires code change) | For new projects, the source generator is recommended. The proxy remains useful when AOT is not a target or per-method [TruncateLog] granularity is specifically needed.


Further Reading

Design rationale and proxy vs. decorator comparison: proxy-vs-decorator.md

Dependencies

  • CoreDesign.Shared for NotFoundMessage and BadRequestMessage result types
  • OneOf for discriminated-union result inspection
  • Microsoft.Extensions.Logging.Abstractions
  • Microsoft.Extensions.DependencyInjection.Abstractions

Feedback

Feedback on this package is welcome. If you run into a missing feature, an unexpected behavior, or something that required more effort than it should have, open an issue at github.com/codyskidmore/CoreDesign/issues or tag @codyskidmore. Suggestions about missing features and priority input are especially appreciated.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 199 5/25/2026
1.0.5 105 5/21/2026 1.0.5 is deprecated because it is no longer maintained and has critical bugs.

1.1.0
- Added source generator that produces compile-time, reflection-free logging decorators for any interface marked with [LoggingDecorator]. Decorators log method entry, exit, parameters, return values, and exceptions with zero runtime overhead compared to handwritten logging decorators. See the CoreDesign.Logging.Generators project for details.
           
1.0.5
- Fixed an issue with log depth and serializaition when there are navigation properties and cicular references.

1.0.4
- Added ILoggable marker interface. Classes implementing ILoggable are discovered by the new assembly-scanning overload of AddWithLogging.
- Added AddWithLogging(IServiceCollection, Assembly, ServiceLifetime) overload that registers all ILoggable classes in the assembly paired with their interfaces.

1.0.2
- Renamed [SensitiveParameter] attribute to [Redact] and renamed [NoLog] to [Suppress] for clarity and consistency with other CoreDesign libraries.

1.0.1
- Initial release