Embrace delegation, composition over inheritance, and mix-in style coding in C# by adopting the implementations of members in your class. TypeAdoption will automatically delegate any unimplemented members to the adopted member.
$ dotnet add package NameHillSoftware.TypeAdoptionAdopt the implementations of members in your class and embrace delegation, composition over inheritance, and mix-in style coding in C#.
When the [Adopt] attribute is attached to a member, the parent class will automatically adopt the interface that the
member is declared as. All interface members will delegate to the adopted member, unless specified in the working class.
For example:
public partial class ConfiguredLogger<T>(ILogger<T> innerLogger)
{
[Adopt]
private readonly ILogger<T> _innerLogger = innerLogger;
public bool IsEnabled(LogLevel logLevel)
{
return logLevel > LogLevel.Debug;
}
// Other ILogger members are automatically delegated to _innerLogger.
}
And what is automatically generated:
// <auto-generated/>
#nullable restore
namespace TypeAdoption.Sample;
public partial class ConfiguredLogger<T> : Microsoft.Extensions.Logging.ILogger<T>
{
#nullable enable
public System.IDisposable? BeginScope<TState>(TState state) where TState : notnull
{
return _innerLogger.BeginScope<TState>(state);
}
#nullable restore
#nullable enable
public void Log<TState>(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, TState state, System.Exception? exception, System.Func<TState, System.Exception?, string> formatter)
{
_innerLogger.Log<TState>(logLevel, eventId, state, exception, formatter);
}
#nullable restore
}
You can keep the adoption secret by setting the Publicly option to false, generating an explicit implementation:
public interface ILoggerConfiguration
{
string? LogTag { get; set; }
int MinLevel { get; set; }
LogLevel GetMinLevel();
}
public class Configuration : ILoggerConfiguration
{
public string? LogTag { get; set; }
public int MinLevel { get; set; }
public LogLevel GetMinLevel()
{
return (LogLevel)MinLevel;
}
}
public partial class ConfiguredLogger<T>(ILogger<T> innerLogger, IConfiguration configuration)
{
[Adopt]
private readonly ILogger<T> _innerLogger = innerLogger;
[Adopt(Publicly = false)]
private readonly IConfiguration _innerConfiguration = configuration;
public bool IsEnabled(LogLevel logLevel)
{
return logLevel >= _innerConfiguration.GetMinLevel();
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (IsEnabled(logLevel))
_innerLogger.Log(logLevel, eventId, state, exception, formatter);
}
}
The automatically generated class will look like this:
// <auto-generated/>
#nullable restore
namespace TypeAdoption.Tests.Lib;
public partial class ConfiguredLogger<T> : TypeAdoption.Tests.Lib.ILoggerConfiguration
{
Microsoft.Extensions.Logging.LogLevel TypeAdoption.Tests.Lib.ILoggerConfiguration.GetMinLevel()
{
return _hiddenConfiguration.GetMinLevel();
}
#nullable enable
string? TypeAdoption.Tests.Lib.ILoggerConfiguration.LogTag { get => _hiddenConfiguration.LogTag; set => _hiddenConfiguration.LogTag = value; }
#nullable restore
int TypeAdoption.Tests.Lib.ILoggerConfiguration.MinLevel { get => _hiddenConfiguration.MinLevel; set => _hiddenConfiguration.MinLevel = value; }
}
The auto-generated logger adoption is reduced to just one method:
// <auto-generated/>
#nullable restore
namespace TypeAdoption.Sample;
public partial class ConfiguredLogger<T> : Microsoft.Extensions.Logging.ILogger<T>
{
#nullable enable
public System.IDisposable? BeginScope<TState>(TState state) where TState : notnull
{
return _innerLogger.BeginScope<TState>(state);
}
#nullable restore
}
And calling the code:
var entity = new AdoptingEntity(new HumanEntity());
using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var logger = new ConfiguredLogger<Program>(loggerFactory.CreateLogger<Program>(), new Configuration
{
MinLevel = LogLevel.Information,
});
// Will log
logger.LogInformation(entity.SayHello());
((ILoggerConfiguration)logger).MinLevel = LogLevel.Warning;
// Won't log
logger.LogInformation(entity.Backpack.ToString());
Normal compiler rules apply — so any conflicts, such as two interfaces with equivalent signatures, will have to be resolved in the actual class.
This project was originally started as a fork of Decorator Generator. All credit to Leopoldo Fu for their well-written library, it provided an excellent base for getting started!
This project is licensed under the Apache License Version 2.0 - see the LICENSE file for details.