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.
dotnet add package CoreDesign.Logging
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.
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.
| 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.
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);
}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);
}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.
Log output size 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.
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.
CoreDesign.SharedforNotFoundMessageandBadRequestMessageresult typesOneOffor discriminated-union result inspectionMicrosoft.Extensions.Logging.AbstractionsMicrosoft.Extensions.DependencyInjection.Abstractions
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.