A collection of C# source generators that automatically generate boilerplate code at compile time, reducing manual coding and improving code maintainability.
This package provides fourteen powerful source generators:
Clone() and DeepClone() method generationIDisposable / IAsyncDisposable pattern generation with ordered resource cleanupEquals, GetHashCode, and operator generationINotifyPropertyChanged/INotifyPropertyChanging implementationToString() override generationInstall the package via NuGet:
dotnet add package BB84.SourceGenerators
Or via Package Manager Console:
Install-Package BB84.SourceGenerators
Generates high-performance extension methods for enumerations, providing faster alternatives to Enum.ToString(), Enum.IsDefined(), Enum.GetNames(), and Enum.GetValues().
[GenerateEnumeratorExtensions]
using BB84.SourceGenerators.Attributes;
[GenerateEnumeratorExtensions]
public enum Status
{
[System.ComponentModel.Description("Pending approval")]
Pending = 0,
Active = 1,
[System.ComponentModel.Description("Temporarily inactive")]
Inactive = 2,
Deleted = 3
}
The generator creates the following extension methods:
ToStringFast() - Returns the name of the enum value as a stringIsDefinedFast(this TEnum value) - Checks if an enum value is definedIsDefinedFast(this string name) - Checks if an enum name is definedGetNamesFast() - Returns all enum names as an IEnumerable<string>GetValuesFast() - Returns all enum values as an IEnumerable<TEnum>GetDescriptionFast() - Returns the description from [Description] attribute, or the name if not presentvar status = Status.Pending;
// Fast string conversion
string name = status.ToStringFast(); // "Pending"
// Check if defined
bool isDefined = status.IsDefinedFast(); // true
bool isNameDefined = "Active".IsDefinedFast(); // true
// Get description
string description = status.GetDescriptionFast(); // "Pending approval"
// Get all names and values
IEnumerable<string> names = status.GetNamesFast();
IEnumerable<Status> values = status.GetValuesFast();
Automatically generates properties with INotifyPropertyChanged and/or INotifyPropertyChanging support for private fields in a class. Both interfaces are independently configurable.
[GenerateNotifications(bool propertyChanged = true, bool propertyChanging = true, bool hasChanged = false)]
Parameters:
propertyChanged - When true (default), implements INotifyPropertyChanged and generates PropertyChanged event and RaisePropertyChanged() methodpropertyChanging - When true (default), implements INotifyPropertyChanging and generates PropertyChanging event and RaisePropertyChanging() methodhasChanged - When true, generates an additional HasChanged boolean property that is set to true when any property changesusing BB84.SourceGenerators.Attributes;
[GenerateNotifications(hasChanged: true)]
public partial class Person
{
private int _id;
private string _name;
private string _email;
private DateTime _createdAt;
private DateTime? _updatedAt;
public Person(int id, string name, string email)
{
_id = id;
_name = name;
_email = email;
_createdAt = DateTime.UtcNow;
}
}
The generator creates:
INotifyPropertyChanged and/or INotifyPropertyChanging interfaces (configurable)PropertyChanged and/or PropertyChanging eventsRaisePropertyChanged() and/or RaisePropertyChanging() methods (protected virtual for non-sealed classes, private for sealed classes)HasChanged property (when hasChanged parameter is true)var person = new Person(1, "John Doe", "john@example.com");
// Subscribe to change notifications
person.PropertyChanging += (s, e) => Console.WriteLine($"Property {e.PropertyName} is changing");
person.PropertyChanged += (s, e) => Console.WriteLine($"Property {e.PropertyName} has changed");
// Change a property - events will fire automatically
person.Name = "Jane Doe";
person.Email = "jane@example.com";
// Check if object has been modified (when hasChanged: true)
if (person.HasChanged)
{
Console.WriteLine("Person has been modified");
}
// Only INotifyPropertyChanged (no PropertyChanging events)
[GenerateNotifications(propertyChanging: false)]
public partial class LightweightModel
{
private string _name;
}
// Works correctly with sealed classes
[GenerateNotifications]
public sealed partial class SealedModel
{
private int _value;
}
Generates interface and implementation classes for static classes, making them testable through dependency injection.
[GenerateAbstraction(Type targetType, Type abstractionType, Type implementationType, params string[] excludeMethods)]
Parameters:
targetType - The static class to generate an abstraction forabstractionType - The interface type to generateimplementationType - The implementation class type to generateexcludeMethods - Optional array of method names to exclude from generationusing BB84.SourceGenerators.Attributes;
// Generate abstraction for System.IO.File
[GenerateAbstraction(typeof(File), typeof(IFileProvider), typeof(FileProvider))]
public partial class FileProvider
{ }
public partial interface IFileProvider
{ }
The generator creates:
IFileProvider) with all public static methods from the target typeFileProvider) that implements the interface and delegates to the static class<inheritdoc>// In your service
public class DocumentService
{
private readonly IFileProvider _fileProvider;
public DocumentService(IFileProvider fileProvider)
=> _fileProvider = fileProvider;
public string ReadDocument(string path)
=> _fileProvider.ReadAllText(path);
}
// In your DI container setup
services.AddSingleton<IFileProvider, FileProvider>();
// In tests, you can mock IFileProvider
var mockFileProvider = new Mock<IFileProvider>();
mockFileProvider.Setup(x => x.ReadAllText(It.IsAny<string>())).Returns("test content");
Generates static Read and Write methods for classes, enabling compile-time INI file serialization and deserialization based on decorated properties.
[GenerateIniFile(StringComparison stringComparison = StringComparison.OrdinalIgnoreCase, string sectionDelimiter = ".")]
[GenerateIniFileSection(string? name = null)]
[GenerateIniFileValue(string? name = null)]
Parameters:
GenerateIniFile - Marks a class for INI file code generation. The optional stringComparison parameter controls how section and key names are compared during deserialization (default: OrdinalIgnoreCase). The optional sectionDelimiter parameter specifies the delimiter for nested section names (default: "."). The optional SerializeComments property, when set to true, includes XML documentation <summary> comments from section and value properties as INI comment lines (prefixed with ; ) in the generated Write output (default: false)GenerateIniFileSection - Marks a property as an INI file section. The optional name parameter specifies the section name; if omitted, the property name is used. Can also be applied to properties within section types to create nested sectionsGenerateIniFileValue - Marks a property as a key-value pair within an INI section. The optional name parameter specifies the key name; if omitted, the property name is usedSupported Value Types:
string, int, long, float, double, bool, decimal, DateTime
using BB84.SourceGenerators.Attributes;
[GenerateIniFile]
public partial class AppConfig
{
[GenerateIniFileSection("General")]
public GeneralSection General { get; set; }
[GenerateIniFileSection("Database")]
public DatabaseSection Database { get; set; }
}
public class GeneralSection
{
[GenerateIniFileValue("AppName")]
public string ApplicationName { get; set; }
[GenerateIniFileValue("Version")]
public int ApplicationVersion { get; set; }
[GenerateIniFileValue("Debug")]
public bool IsDebug { get; set; }
}
public class DatabaseSection
{
[GenerateIniFileValue("Server")]
public string Server { get; set; }
[GenerateIniFileValue("Port")]
public int Port { get; set; }
[GenerateIniFileValue("Timeout")]
public double Timeout { get; set; }
}
The generator creates the following static methods on the decorated class:
Read(string content) - Parses an INI file string and returns a deserialized instanceWrite(TClass instance) - Serializes an instance into an INI file string// Reading an INI file
string iniContent = File.ReadAllText("config.ini");
AppConfig config = AppConfig.Read(iniContent);
Console.WriteLine(config.General.ApplicationName); // "MyApp"
Console.WriteLine(config.Database.Port); // 5432
// Modifying and writing back
config.General.Debug = false;
config.Database.Timeout = 60.0;
string output = AppConfig.Write(config);
File.WriteAllText("config.ini", output);
Output:
[General]
AppName=MyApp
Version=1
Debug=False
[Database]
Server=localhost
Port=5432
Timeout=60
Case-Sensitive Matching:
By default, section and key names are compared case-insensitively (OrdinalIgnoreCase). To use case-sensitive matching:
[GenerateIniFile(StringComparison.Ordinal)]
public partial class StrictConfig
{
[GenerateIniFileSection("General")]
public GeneralSection General { get; set; }
}
Nested Sections:
The generator supports nested sections by applying [GenerateIniFileSection] to properties within section types. Nested sections are represented with dotted names (e.g., [Server.Database]), and nesting is supported up to 8 levels deep:
[GenerateIniFile]
public partial class Config
{
[GenerateIniFileSection]
public ServerSection Server { get; set; }
}
public class ServerSection
{
[GenerateIniFileValue]
public string Host { get; set; }
[GenerateIniFileSection]
public DatabaseSection Database { get; set; }
}
public class DatabaseSection
{
[GenerateIniFileValue]
public int Port { get; set; }
}
This produces:
[Server]
Host=localhost
[Server.Database]
Port=5432
To use a custom delimiter (e.g., /):
[GenerateIniFile(sectionDelimiter: "/")]
public partial class Config { ... }
// Produces: [Server/Database]
Serializing Comments:
When SerializeComments is set to true, XML documentation <summary> comments on section and value properties are emitted as INI comment lines (prefixed with ; ) in the Write output. The Read method automatically skips these comment lines during deserialization:
[GenerateIniFile(SerializeComments = true)]
public partial class AppConfig
{
/// <summary>
/// General application settings
/// </summary>
[GenerateIniFileSection]
public GeneralSection General { get; set; }
}
public class GeneralSection
{
/// <summary>
/// The display name of the application
/// </summary>
[GenerateIniFileValue]
public string AppName { get; set; }
/// <summary>
/// The current version number
/// </summary>
[GenerateIniFileValue]
public int Version { get; set; }
}
This produces:
; General application settings
[General]
; The display name of the application
AppName=MyApp
; The current version number
Version=1
Enumeration Support:
Regular enums will be serialized as their string name. Enum flags will be serialized as a space-delimited string of individual flag names.
public enum LogLevel { Debug, Info, Warning, Error }
[Flags]
public enum Permissions { None = 0, Read = 1, Write = 2, Execute = 4 }
public class AppSection
{
[GenerateIniFileValue]
public LogLevel Level { get; set; }
[GenerateIniFileValue]
public Permissions Perms { get; set; }
}
This produces:
[App]
Level=Warning
Perms=Read Write Execute
Generates a fluent builder class for classes, providing With{PropertyName}(value) methods for each public settable property and a Build() method that creates the instance.
[GenerateBuilder]
using BB84.SourceGenerators.Attributes;
[GenerateBuilder]
public partial class UserProfile
{
public int Id { get; set; }
public string Name { get; set; }
public string? Email { get; set; }
public int Age { get; set; }
public bool IsActive { get; set; }
}
The generator creates a {ClassName}Builder class with:
With{PropertyName}(value) fluent method for each public settable propertyBuild() method that creates the instance via object initializer// Create an instance using the fluent builder
UserProfile profile = new UserProfileBuilder()
.WithId(1)
.WithName("John Doe")
.WithEmail("john@example.com")
.WithAge(30)
.WithIsActive(true)
.Build();
// Only set the properties you need - others use default values
UserProfile minimal = new UserProfileBuilder()
.WithName("Jane Doe")
.Build();
// Builders can be reused to create multiple instances
var builder = new UserProfileBuilder()
.WithName("Template User")
.WithIsActive(true);
UserProfile first = builder.WithId(1).Build();
UserProfile second = builder.WithId(2).Build();
Generates a ToString() override for classes, returning a formatted string containing the class name and all (or selected) public readable property values in the format ClassName { Prop1 = val1, Prop2 = val2 }.
[GenerateToString(params string[] excludeProperties)]
Parameters:
excludeProperties - Optional list of property names to exclude from the generated ToString() outputusing BB84.SourceGenerators.Attributes;
[GenerateToString]
public partial class Product
{
public int Id { get; set; }
public string Name { get; set; }
public double Price { get; set; }
public bool InStock { get; set; }
}
// Exclude sensitive or verbose properties
[GenerateToString("PasswordHash", "InternalNotes")]
public partial class User
{
public int Id { get; set; }
public string Name { get; set; }
public string PasswordHash { get; set; }
public string InternalNotes { get; set; }
}
The generator creates a ToString() override on the partial class that:
ClassName { Prop1 = val1, Prop2 = val2 }ClassName { } when all properties are excludedvar product = new Product
{
Id = 1,
Name = "Widget",
Price = 9.99,
InStock = true
};
Console.WriteLine(product.ToString());
// Output: Product { Id = 1, Name = Widget, Price = 9.99, InStock = True }
var user = new User
{
Id = 42,
Name = "John Doe",
PasswordHash = "abc123hash",
InternalNotes = "VIP customer"
};
Console.WriteLine(user.ToString());
// Output: User { Id = 42, Name = John Doe }
Generates a Validate() method for classes, scanning properties for data annotation attributes at compile time and emitting direct validation checks. This replaces runtime reflection-based Validator.TryValidateObject() with zero-overhead generated code.
[GenerateValidator]
Supported Data Annotations:
[Required] - Validates that the property is not null (or not null/empty for strings)[Range(min, max)] - Validates that a numeric value falls within a specified range, or that a collection has between min and max elements[StringLength(max, MinimumLength = min)] - Validates string length within bounds[MinLength(length)] - Validates minimum length of a string or collection[MaxLength(length)] - Validates maximum length of a string or collection[RegularExpression(pattern)] - Validates that a string matches a regex patternConstraints:
BB84SG0001) will be emitted if used on an abstract class.using System.ComponentModel.DataAnnotations;
using BB84.SourceGenerators.Attributes;
[GenerateValidator]
public partial class UserRegistration
{
[Required]
[StringLength(100, MinimumLength = 2)]
public string? Name { get; set; }
[Required]
[RegularExpression(@"^[^@\s]+@[^@\s]+\.[^@\s]+$")]
public string? Email { get; set; }
[Range(18, 120)]
public int Age { get; set; }
[Required]
[MinLength(8)]
[MaxLength(128)]
public string? Password { get; set; }
[Range(1, 10)]
public List<int>? Skills { get; set; }
}
The generator creates the following methods on the partial class:
Validate() - Returns a Dictionary<string, List<string>> where each key is a property name and the value is a list of validation error messages for that property. An empty dictionary indicates a valid instance.Validate(string propertyName) - Returns a List<string> of validation error messages for the specified property. An empty list indicates the property is valid.Both methods contain direct if-checks for each data annotation rule and support custom error messages via ErrorMessage property.
var registration = new UserRegistration
{
Name = "J",
Email = "not-an-email",
Age = 15,
Password = "short"
};
Dictionary<string, List<string>> errors = registration.Validate();
if (errors.Count > 0)
{
foreach (KeyValuePair<string, List<string>> entry in errors)
{
Console.WriteLine($"{entry.Key}:");
foreach (string error in entry.Value)
{
Console.WriteLine($" - {error}");
}
}
// Output:
// Name:
// - The field Name must be a string with a minimum length of 2 and a maximum length of 100.
// Email:
// - The field Email must match the regular expression '^[^@\s]+@[^@\s]+\.[^@\s]+$'.
// Age:
// - The field Age must be between 18 and 120.
// Password:
// - The field Password must be a string or collection with a minimum length of 8.
}
else
{
Console.WriteLine("Registration is valid!");
}
// Validate a single property
List<string> nameErrors = registration.Validate("Name");
if (nameErrors.Count > 0)
{
Console.WriteLine($"Name errors: {string.Join(", ", nameErrors)}");
}
// Custom error messages
[GenerateValidator]
public partial class LoginModel
{
[Required(ErrorMessage = "Username is required.")]
public string? Username { get; set; }
[Required(ErrorMessage = "Password cannot be empty.")]
[MinLength(6, ErrorMessage = "Password must be at least 6 characters.")]
public string? Password { get; set; }
}
Generates Equals(object), Equals(T), GetHashCode(), operator ==, and operator != for classes, implementing IEquatable<T>. This replaces tedious and error-prone manual equality implementations with zero-overhead generated code.
[GenerateEquality(params string[] excludeProperties)]
Parameters:
excludeProperties - Optional list of property names to exclude from the generated equality comparisonusing BB84.SourceGenerators.Attributes;
[GenerateEquality]
public partial class Product
{
public int Id { get; set; }
public string? Name { get; set; }
public double Price { get; set; }
public bool InStock { get; set; }
}
// Exclude volatile or non-significant properties
[GenerateEquality("CachedHash", "LastAccessed")]
public partial class User
{
public int Id { get; set; }
public string? Name { get; set; }
public string? CachedHash { get; set; }
public DateTime LastAccessed { get; set; }
}
The generator creates the following members on the partial class:
Equals(object?) override — delegates to the typed Equals(T?) methodEquals(T?) implementing IEquatable<T> — compares all (or selected) public propertiesGetHashCode() override — produces a consistent hash from all (or selected) public propertiesoperator == and operator != — delegates to Equalsvar a = new Product { Id = 1, Name = "Widget", Price = 9.99, InStock = true };
var b = new Product { Id = 1, Name = "Widget", Price = 9.99, InStock = true };
var c = new Product { Id = 2, Name = "Gadget", Price = 19.99, InStock = false };
// Typed equality
bool equal = a.Equals(b); // true
bool different = a.Equals(c); // false
// Operator equality
bool op = a == b; // true
bool neq = a != c; // true
// Consistent hash codes
bool sameHash = a.GetHashCode() == b.GetHashCode(); // true
// Works with collections that use equality
var set = new HashSet<Product> { a };
bool contains = set.Contains(b); // true
// IEquatable<T> is implemented
IEquatable<Product> equatable = a;
Generates Clone() and DeepClone() methods for classes, implementing ICloneable. The Clone() method performs a shallow copy, while DeepClone() recursively deep clones reference-type properties that are also marked with [GenerateCloneable].
[GenerateCloneable(params string[] excludeProperties)]
Parameters:
excludeProperties - Optional list of property names to exclude from the generated clone methodsusing BB84.SourceGenerators.Attributes;
[GenerateCloneable]
public partial class UserProfile
{
public int Id { get; set; }
public string? Name { get; set; }
public double Score { get; set; }
public Address? Address { get; set; }
}
[GenerateCloneable]
public partial class Address
{
public string? Street { get; set; }
public string? City { get; set; }
}
// Exclude specific properties from cloning
[GenerateCloneable("CacheToken")]
public partial class Session
{
public int Id { get; set; }
public string? User { get; set; }
public string? CacheToken { get; set; }
}
The generator creates the following members on the partial class:
Clone() — creates a shallow copy by assigning all included public read/write propertiesDeepClone() — creates a deep copy; reference-type properties marked with [GenerateCloneable] are recursively deep cloned, while other properties are shallow copiedICloneable.Clone() — delegates to DeepClone()var original = new UserProfile
{
Id = 1,
Name = "John Doe",
Score = 95.5,
Address = new Address { Street = "123 Main St", City = "Springfield" }
};
// Shallow clone - Address is the same reference
UserProfile shallow = original.Clone();
shallow.Address.City = "Shelbyville";
Console.WriteLine(original.Address.City); // "Shelbyville" (shared reference)
// Deep clone - Address is a new independent instance
UserProfile deep = original.DeepClone();
deep.Address.City = "Capital City";
Console.WriteLine(original.Address.City); // "Shelbyville" (unaffected)
// ICloneable is implemented (delegates to DeepClone)
ICloneable cloneable = original;
object copy = cloneable.Clone();
// Excluded properties are not copied
var session = new Session { Id = 1, User = "admin", CacheToken = "abc123" };
Session clonedSession = session.Clone();
Console.WriteLine(clonedSession.CacheToken); // null
Generates compile-time constant properties for assembly metadata (title, version, company, copyright, etc.), eliminating the need for runtime reflection via Assembly.GetCustomAttribute<T>().
[GenerateAssemblyInformation]
using BB84.SourceGenerators.Attributes;
[GenerateAssemblyInformation]
public partial class AppInfo
{ }
The generator creates public const string properties for each standard assembly attribute:
Title — from AssemblyTitleAttributeDescription — from AssemblyDescriptionAttributeCompany — from AssemblyCompanyAttributeProduct — from AssemblyProductAttributeCopyright — from AssemblyCopyrightAttributeTrademark — from AssemblyTrademarkAttributeConfiguration — from AssemblyConfigurationAttributeVersion — from AssemblyVersionAttributeFileVersion — from AssemblyFileVersionAttributeInformationalVersion — from AssemblyInformationalVersionAttributeConsole.WriteLine(AppInfo.Title); // "MyApp"
Console.WriteLine(AppInfo.Version); // "1.0.0.0"
Console.WriteLine(AppInfo.Company); // "My Company"
Console.WriteLine(AppInfo.Copyright); // "Copyright © 2025"
Console.WriteLine(AppInfo.InformationalVersion); // "1.0.0+abc123"
Generates the singleton pattern for classes, providing a static Instance property and a private constructor. Supports thread-safe lazy initialization via Lazy<T> (default) or simple static field initialization. When the class implements an interface, the Instance property is typed as the interface.
[GenerateSingleton(bool useLazy = true)]
Parameters:
useLazy - When true (default), the singleton is backed by Lazy<T> for thread-safe lazy initialization. When false, a simple static readonly field is used instead.Constraints:
required fields or properties. A compile-time error (BB84SG0002) will be emitted if the class contains any required members that prevent parameterless initialization.using BB84.SourceGenerators.Attributes;
// Lazy singleton (default)
[GenerateSingleton]
public partial class MyService { }
// Non-lazy singleton
[GenerateSingleton(useLazy: false)]
public partial class MyCache { }
// Singleton with interface
[GenerateSingleton]
internal partial class MyService : IMyService { }
public partial class MyService
{
private static readonly Lazy<MyService> _lazyInstance = new Lazy<MyService>(() => new MyService());
/// <summary>
/// Gets the singleton instance of <see cref="MyService"/>.
/// </summary>
public static MyService Instance => _lazyInstance.Value;
private MyService() { }
}
internal partial class MyService
{
private static readonly Lazy<MyService> _lazyInstance = new Lazy<MyService>(() => new MyService());
/// <summary>
/// Gets the singleton instance of <see cref="MyService"/> as <see cref="IMyService"/>.
/// </summary>
public static IMyService Instance => _lazyInstance.Value;
private MyService() { }
}
public partial class MyCache
{
/// <summary>
/// Gets the singleton instance of <see cref="MyCache"/>.
/// </summary>
public static MyCache Instance { get; } = new MyCache();
private MyCache() { }
}
// Access the singleton instance
MyService service = MyService.Instance;
// With interface typing
IMyService service = MyService.Instance;
// Thread-safe - always returns the same instance
MyService a = MyService.Instance;
MyService b = MyService.Instance;
// a and b are the same reference
Generates a decorator class that wraps an inner instance, delegates all interface members to it, and exposes them as virtual methods and properties for selective overriding. This replaces runtime proxy generation (e.g., DispatchProxy, Castle.Core DynamicProxy) with zero-overhead compile-time code.
[GenerateDecorator]
using BB84.SourceGenerators.Attributes;
public interface IMyService
{
string? Name { get; set; }
int Count { get; }
string GetMessage(string input);
void DoWork();
}
[GenerateDecorator]
public partial class MyServiceDecorator : IMyService
{ }
The generator creates the following members on the partial class:
_inner)// Basic delegation - all calls forwarded to inner instance
var inner = new MyService();
var decorator = new MyServiceDecorator(inner);
decorator.DoWork(); // delegates to inner.DoWork()
string msg = decorator.GetMessage("Hi"); // delegates to inner.GetMessage("Hi")
// Override specific behavior by inheriting from the decorator
public class LoggingDecorator(IMyService inner) : MyServiceDecorator(inner)
{
public override string GetMessage(string input)
{
Console.WriteLine($"GetMessage called with: {input}");
return base.GetMessage(input);
}
public override string? Name
{
get => base.Name?.ToUpperInvariant();
set => base.Name = value;
}
}
// Multiple interfaces are supported
public interface ICalculator
{
int Calculate(int a, int b);
}
[GenerateDecorator]
public partial class MultiDecorator : IMyService, ICalculator
{ }
// In your DI container
services.AddSingleton<IMyService>(sp =>
new LoggingDecorator(sp.GetRequiredService<MyService>()));
Generates a factory class that discovers all concrete implementations of a target interface at compile time and emits a Create(string key) method that maps keys to concrete types. This eliminates runtime assembly scanning and manual factory maintenance entirely.
[GenerateFactory(Type interfaceType)]
[GenerateFactoryKey(string key)]
Parameters:
GenerateFactory - Marks a partial class for factory code generation. The interfaceType parameter specifies the interface whose implementations should be discovered.GenerateFactoryKey - Optional attribute placed on implementation classes to specify a custom key. When not present, the class name is used as the key.Constraints:
interfaceType must be an interface. A compile-time error (BB84SG0004) will be emitted if a non-interface type is specified.BB84SG0005).using BB84.SourceGenerators.Attributes;
public interface IAnimal
{
string Speak();
}
public class Dog : IAnimal
{
public string Speak() => "Woof!";
}
public class Cat : IAnimal
{
public string Speak() => "Meow!";
}
[GenerateFactory(typeof(IAnimal))]
public partial class AnimalFactory
{ }
The generator creates the following static methods on the partial class:
Create(string key) - Creates an instance of the target interface based on the specified key using a switch expression. Throws ArgumentException for unknown keys.GetKeys() - Returns an IEnumerable<string> of all registered factory keys.// Create instances by type name (default key)
IAnimal dog = AnimalFactory.Create("Dog");
IAnimal cat = AnimalFactory.Create("Cat");
Console.WriteLine(dog.Speak()); // "Woof!"
Console.WriteLine(cat.Speak()); // "Meow!"
// Unknown keys throw ArgumentException
try
{
AnimalFactory.Create("Fish");
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message); // "No implementation registered for key 'Fish'."
}
// Enumerate all registered keys
foreach (string key in AnimalFactory.GetKeys())
{
Console.WriteLine(key); // "Dog", "Cat"
}
// Use custom keys with [GenerateFactoryKey]
public interface IVehicle
{
string Type { get; }
}
[GenerateFactoryKey("sedan")]
public class Car : IVehicle
{
public string Type => "Car";
}
[GenerateFactoryKey("pickup")]
public class Truck : IVehicle
{
public string Type => "Truck";
}
[GenerateFactory(typeof(IVehicle))]
public partial class VehicleFactory
{ }
// Create using custom keys
IVehicle vehicle = VehicleFactory.Create("sedan");
Console.WriteLine(vehicle.Type); // "Car"
Generates the complete dispose pattern for classes, implementing IDisposable and optionally IAsyncDisposable. Fields marked with [DisposeResource] are automatically disposed with configurable ordering.
[GenerateDisposable(bool generateFinalizer = false, bool async = false)]
[DisposeResource(int order = 0)]
Parameters:
GenerateDisposable:
generateFinalizer - When true, generates a finalizer that calls Dispose(false)async - When true, additionally implements IAsyncDisposable with DisposeAsync() and DisposeAsyncCore()DisposeResource:
order - Disposal order. Non-zero values are disposed first (ascending), then zero-order fields in source declaration orderConstraints:
GenerateDisposable can only be applied to classes.DisposeResource can only be applied to fields.IAsyncDisposable is only implemented when async: true is specified. A compile-time error (BB84SG0003) will be emitted if the framework does not support IAsyncDisposable and async: true is used.using BB84.SourceGenerators.Attributes;
[GenerateDisposable]
public partial class ConnectionManager
{
[DisposeResource]
private Stream _stream;
[DisposeResource(order: 1)]
private DbConnection _connection;
private string _name; // not disposed (no attribute)
public ConnectionManager(Stream stream, DbConnection connection)
{
_stream = stream;
_connection = connection;
}
}
The generator creates:
private bool _disposed guard fieldDispose(bool disposing) method (protected virtual for non-sealed, private for sealed classes)Dispose() implementing IDisposableThrowIfDisposed() guard methodgenerateFinalizer: true)DisposeAsync() and DisposeAsyncCore() (when async: true)// Basic usage
var manager = new ConnectionManager(stream, connection);
// ... use resources ...
manager.Dispose(); // _connection disposed first (order 1), then _stream (order 0)
// With finalizer for unmanaged resources
[GenerateDisposable(generateFinalizer: true)]
public partial class NativeResourceWrapper
{
[DisposeResource]
private SafeHandle _handle;
}
// With async disposal
[GenerateDisposable(async: true)]
public partial class AsyncService
{
[DisposeResource]
private HttpClient _client;
[DisposeResource(order: 1)]
private DbConnection _connection;
}
await using var service = new AsyncService(client, connection);
// Sealed classes work correctly (private methods instead of protected virtual)
[GenerateDisposable]
public sealed partial class SealedResource
{
[DisposeResource]
private Stream _stream;
}
// Ordered disposal - dispose writer before underlying stream
[GenerateDisposable]
public partial class FileProcessor
{
[DisposeResource(order: 2)]
private Stream _stream;
[DisposeResource(order: 1)]
private StreamWriter _writer;
}
The generated enum extension methods provide significant performance improvements over reflection-based Enum methods:
CultureInfo.InvariantCulture for consistent cross-platform formattingToString() override at compile timetypeof(T).GetProperties().Select(...)) with zero-overhead generated codeif-checks at compile time for each data annotation ruleValidator.TryValidateObject() which uses TypeDescriptor and reflection at runtimeEquals, GetHashCode, ==, and != implementations at compile timeClone() and DeepClone() methods at compile time with zero runtime overheadMemberwiseClone (shallow only), serialization round-trips (slow), and manual clone implementations (error-prone)[GenerateCloneable]ICloneable for framework compatibilityconst string properties at compile time for all standard assembly attributesAssembly.GetCustomAttribute<T>()<AssemblyTitle>, <Version>, etc.)Lazy<T>-backed (default) and simple static field initializationInstance as the implemented interface when presentrequired membersDispatchProxy, Castle.Core DynamicProxy, and manual decorator implementationsAssembly.GetTypes() and reflectionCreate(string key) method using switch expressions for zero-overhead dispatch[GenerateFactoryKey] attributeIDisposable implementations (missed fields, wrong guard logic, missing GC.SuppressFinalize)[DisposeResource(order)] for expressing resource dependenciesIAsyncDisposable with DisposeAsync() and DisposeAsyncCore()private vs protected virtual)Source generators run during compilation and generate additional C# source files that are compiled alongside your code. This means:
Contributions are welcome! If you have an idea for a new feature, improvement, or bug fix, please follow these steps:
We expect all contributors to adhere to the Code of Conduct.
This project is licensed under the MIT License - see the LICENSE file for details.
Robert Peter Meyer (BoBoBaSs84)
See releases for version history and changelog.