BB84.SourceGenerators

A collection of C# source generators that automatically generate boilerplate code at compile time, reducing manual coding and improving code maintainability.

CI CD CodeQL Dependabot

License: MIT C# .NET Standard 2.0 Issues LastCommit PullRequests RepoSize NuGet

Features

This package provides fifteen powerful source generators:

Installation

Install the package via NuGet:

dotnet add package BB84.SourceGenerators

Or via Package Manager Console:

Install-Package BB84.SourceGenerators

In the project where you want to use the generators, add a reference to the package and include the appropriate using directive for the attributes:

using BB84.SourceGenerators.Attributes;

If the attributes are not recognized, make shure that the EmitCompilerGeneratedFiles property is set to true in the project file:

<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

Usage

1. Enumerator Extensions Generator

Generates high-performance extension methods for enumerations, providing faster alternatives to Enum.ToString(), Enum.IsDefined(), Enum.GetNames(), and Enum.GetValues().

Attribute

[GenerateEnumeratorExtensions]

Example

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
}

Generated Methods

The generator creates the following extension methods:

Usage Example

var status = Status.Pending;

// Fast string conversion
string name = status.ToStringFast(); // "Pending"

// Check if defined
bool isDefined = status.IsDefinedFast(); // true
bool isNameDefined = StatusExtensions.IsDefinedFast("Active"); // true

// Get description
string description = status.GetDescriptionFast(); // "Pending approval"

// Get all names and values
IEnumerable<string> names = StatusExtensions.GetNamesFast();
IEnumerable<Status> values = StatusExtensions.GetValuesFast();

2. Notification Properties Generator

Automatically generates properties with INotifyPropertyChanged and/or INotifyPropertyChanging support for private fields in a class. Both interfaces are independently configurable.

Attributes

[GenerateNotifications(bool propertyChanged = true, bool propertyChanging = true, bool hasChanged = false)]
[AlsoNotify(params string[] propertyNames)]
[ExcludeFromNotification]

Parameters:

Field-Level Attributes:

Example

using BB84.SourceGenerators.Attributes;

[GenerateNotifications(hasChanged: true)]
public partial class Person
{
    private int _id;

    [AlsoNotify(nameof(FullName))]
    private string _firstName;

    [AlsoNotify(nameof(FullName))]
    private string _lastName;

    private string _fullName;

    [ExcludeFromNotification]
    private string _internalToken;

    private DateTime _createdAt;
    private DateTime? _updatedAt;

    public Person(int id, string firstName, string lastName)
    {
        _id = id;
        _firstName = firstName;
        _lastName = lastName;
        _createdAt = DateTime.UtcNow;
    }

		public string FullName => $"{_firstName} {_lastName}";
}

Generated Code

The generator creates:

Usage Example

var person = new Person(1, "John", "Doe");

// 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 FirstName - fires notifications for both FirstName and FullName
person.FirstName = "Jane";
// Output:
// Property FirstName is changing
// Property FullName is changing
// Property FirstName has changed
// Property FullName has changed

// Check if object has been modified (when hasChanged: true)
if (person.HasChanged)
{
    Console.WriteLine("Person has been modified");
}

// Fields with [ExcludeFromNotification] have no generated property
// person.InternalToken  <-- does not exist

// 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;
}

3. Abstraction Generator

Generates interface and implementation classes for static classes, making them testable through dependency injection.

Attribute

[GenerateAbstraction(Type targetType, Type abstractionType, Type implementationType)]

Parameters:

Properties:

Example

using BB84.SourceGenerators.Attributes;

// Generate abstraction for System.IO.File
[GenerateAbstraction(typeof(File), typeof(IFileProvider), typeof(FileProvider))]
public partial class FileProvider
{ }

public partial interface IFileProvider
{ }

// Generate abstraction with exclusions
[GenerateAbstraction(typeof(Environment), typeof(IEnvironmentProvider), typeof(EnvironmentProvider),
    ExcludeMethods = ["Exit", "FailFast"],
    ExcludeProperties = ["ExitCode"])]
public partial class EnvironmentProvider
{ }

public partial interface IEnvironmentProvider
{ }

Generated Code

The generator creates:

Usage Example

// 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");

4. INI File Generator

Generates static Read and Write methods and an instance Load method for classes, enabling compile-time INI file serialization, deserialization, and instance-to-instance value copying based on decorated properties.

Attributes

[GenerateIniFile(StringComparison stringComparison = StringComparison.OrdinalIgnoreCase, string sectionDelimiter = ".")]
[GenerateIniFileSection(string? name = null)]
[GenerateIniFileValue(string? name = null)]

Parameters:

Supported Value Types: string, int, long, float, double, bool, decimal, DateTime, Guid, TimeSpan, DateTimeOffset

Collection Support: List<T> and T[] are supported for any of the above value types. Values are serialized and deserialized as comma-separated strings.

Example

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; }
}

Generated Methods

The generator creates the following methods on the decorated class:

Usage Example

// 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);

// Loading values from another instance
string fileContent = File.ReadAllText("updated.ini");
AppConfig newConfig = AppConfig.Read(fileContent);
config.Load(newConfig); // copies all section values from newConfig into config

// Safe parsing with TryRead (no exceptions)
if (AppConfig.TryRead(iniContent, out AppConfig? parsed))
{
    Console.WriteLine(parsed.General.ApplicationName);
}
else
{
    Console.WriteLine("Failed to parse INI content");
}

// Async reading from a file path (line-by-line, truly non-blocking)
AppConfig asyncConfig = await AppConfig.ReadAsync("config.ini");

// Async reading from a stream
using FileStream fs = File.OpenRead("config.ini");
AppConfig asyncConfigFromStream = await AppConfig.ReadAsync(fs);

// Async writing to a stream
using FileStream outFs = File.Create("output.ini");
await AppConfig.WriteAsync(asyncConfig, outFs);

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

Collection Support:

List<T> and T[] properties are serialized as comma-separated values:

public class SettingsSection
{
    [GenerateIniFileValue]
    public List<string>? Tags { get; set; }

    [GenerateIniFileValue]
    public int[]? Ports { get; set; }

    [GenerateIniFileValue]
    public List<Guid>? Identifiers { get; set; }
}

This produces:

[Settings]
Tags=web,api,backend
Ports=8080,8443,9090
Identifiers=d3b07384-d9a0-4e9b-8c12-f7a8b2c1d3e5,a1b2c3d4-e5f6-7890-abcd-ef1234567890

5. Builder Generator

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.

Attribute

[GenerateBuilder]

Example

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; }
}

Generated Code

The generator creates a {ClassName}Builder class with:

Usage Example

// 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();

6. ToString Generator

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 }. Collection properties (lists, arrays, dictionaries) are rendered with configurable formatting instead of the default type name.

Attribute

[GenerateToString(params string[] excludeProperties)]
[ToStringFormat(string format)]  // per-property
[ToStringOrder(int order)]       // per-property

Parameters:

Per-Property Formatting:

Collection Format Modes:

Mode Description Example output
CollectionFormat.Count (default) Shows element count only Count = 3
CollectionFormat.Elements Inline elements for lists/arrays; {key: value} pairs for dictionaries [Alice, Bob] / {Alice: 10}
CollectionFormat.TypeAndCount Shows runtime type name and element count List\1 (Count = 3)`

null collections are always rendered as null regardless of the format mode.

Example

using 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; }
}

// Collection properties — explicit Elements mode for full display
[GenerateToString(CollectionFormat = CollectionFormat.Elements)]
public partial class Team
{
    public string Name { get; set; }
    public List<string> Members { get; set; }
    public Dictionary<string, int> Scores { get; set; }
}

// Collection properties — Count mode
[GenerateToString(CollectionFormat = CollectionFormat.Count)]
public partial class TeamSummary
{
    public string Name { get; set; }
    public List<string> Members { get; set; }
}

// Per-property format specifiers
[GenerateToString]
public partial class Order
{
    public int Id { get; set; }

    [ToStringFormat("yyyy-MM-dd")]
    public DateTime CreatedAt { get; set; }

    [ToStringFormat("C2")]
    public decimal Total { get; set; }
}

// Custom property separator
[GenerateToString(Separator = " | ")]
public partial class Config
{
    public string Host { get; set; }
    public int Port { get; set; }
}

// IFormattable implementation for culture-aware formatting
[GenerateToString(Formattable = true)]
public partial class Invoice
{
    public string Customer { get; set; }
    public decimal Total { get; set; }
    public DateTime IssuedAt { get; set; }
}

// Explicit null rendering for nullable properties
[GenerateToString(NullPlaceholder = "null")]
public partial class Person
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public int? Age { get; set; }
}

// Explicit property ordering — ordered properties first, then unordered in declaration order
[GenerateToString]
public partial class Contact
{
    [ToStringOrder(2)]
    public string? LastName { get; set; }

    [ToStringOrder(1)]
    public string? FirstName { get; set; }

    public int Age { get; set; }
}

// Include inherited properties from base classes
public class EntityBase
{
    public int Id { get; set; }
    public string? CreatedBy { get; set; }
}

[GenerateToString(IncludeInherited = true)]
public partial class Order : EntityBase
{
    public decimal Total { get; set; }
    public string? Reference { get; set; }
}

Generated Code

The generator creates a ToString() override on the partial class that:

Usage Example

var 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 }

var team = new Team
{
    Name = "Alpha",
    Members = new List<string> { "Alice", "Bob", "Charlie" },
    Scores = new Dictionary<string, int> { ["Alice"] = 10, ["Bob"] = 20 }
};

Console.WriteLine(team.ToString());
// Output: Team { Name = Alpha, Members = [Alice, Bob, Charlie], Scores = {Alice: 10, Bob: 20} }

var summary = new TeamSummary
{
    Name = "Beta",
    Members = new List<string> { "Alice", "Bob", "Charlie" }
};

Console.WriteLine(summary.ToString());
// Output: TeamSummary { Name = Beta, Members = Count = 3 }

var order = new Order
{
    Id = 1,
    CreatedAt = new DateTime(2025, 1, 15),
    Total = 1234.56m
};

Console.WriteLine(order.ToString());
// Output: Order { Id = 1, CreatedAt = 2025-01-15, Total = $1,234.56 }

var config = new Config
{
    Host = "localhost",
    Port = 8080
};

Console.WriteLine(config.ToString());
// Output: Config { Host = localhost | Port = 8080 }

var invoice = new Invoice
{
    Customer = "Acme Corp",
    Total = 1234.56m,
    IssuedAt = new DateTime(2025, 1, 15)
};

// IFormattable: pass a custom format provider
Console.WriteLine(invoice.ToString(null, CultureInfo.InvariantCulture));
// Output: Invoice { Customer = Acme Corp, Total = 1234.56, IssuedAt = 01/15/2025 00:00:00 }

// Works with string.Format and interpolated strings
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, "{0}", invoice));

var person = new Person { Id = 1, Name = null, Age = null };
Console.WriteLine(person.ToString());
// Output: Person { Id = 1, Name = null, Age = null }

var personWithValues = new Person { Id = 2, Name = "John", Age = 30 };
Console.WriteLine(personWithValues.ToString());
// Output: Person { Id = 2, Name = John, Age = 30 }

var contact = new Contact { LastName = "Doe", FirstName = "John", Age = 30 };
Console.WriteLine(contact.ToString());
// Output: Contact { FirstName = John, LastName = Doe, Age = 30 }

7. Validator Generator

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.

Attribute

[GenerateValidator]

Supported Data Annotations:

IValidatableObject Integration:

When a class implements IValidatableObject, the generated Validate() method automatically calls IValidatableObject.Validate(ValidationContext) and merges the results into the error dictionary.

Constraints:

Example

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; }
}

Generated Code

The generator creates the following methods on the partial class:

Both methods contain direct if-checks for each data annotation rule and support custom error messages via ErrorMessage property.

Usage Example

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; }
}

// Cross-property validation with [Compare]
[GenerateValidator]
public partial class RegistrationModel
{
    [Required]
    [EmailAddress]
    public string? Email { get; set; }

    [Required]
    [MinLength(8)]
    public string? Password { get; set; }

    [Compare(nameof(Password), ErrorMessage = "Passwords do not match.")]
    public string? ConfirmPassword { get; set; }

    [Url]
    public string? Website { get; set; }

    [Phone]
    public string? PhoneNumber { get; set; }

    [CreditCard]
    public string? PaymentCard { get; set; }
}

// Custom ValidationAttribute subclasses
public sealed class EvenNumberAttribute : ValidationAttribute
{
    public override bool IsValid(object? value)
        => value is int n && n % 2 == 0;
}

[GenerateValidator]
public partial class CustomAttrModel
{
    [EvenNumber(ErrorMessage = "Must be even.")]
    public int Value { get; set; }
}

// IValidatableObject integration
[GenerateValidator]
public partial class DateRangeModel : IValidatableObject
{
    [Required]
    public DateTime Start { get; set; }

    [Required]
    public DateTime End { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext context)
    {
        if (End <= Start)
            yield return new ValidationResult("End must be after Start.", new[] { nameof(End) });
    }
}

// AllowedValues / DeniedValues (.NET 8+) — inline membership/exclusion checks
[GenerateValidator]
public partial class OrderModel
{
    // Emits: if (!Equals(Status, "pending") && !Equals(Status, "shipped") && !Equals(Status, "delivered"))
    [AllowedValues("pending", "shipped", "delivered", ErrorMessage = "Invalid order status.")]
    public string? Status { get; set; }

    // Emits: if (Equals(Priority, 0) || Equals(Priority, -1))
    [DeniedValues(0, -1, ErrorMessage = "Priority must be positive.")]
    public int Priority { get; set; }
}

8. Equality Generator

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.

Attribute

[GenerateEquality(params string[] excludeProperties)]

Parameters:

Example

using 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; }
}

Generated Code

The generator creates the following members on the partial class:

Usage Example

var 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;

9. Cloneable Generator

Generates Clone() and DeepClone() methods for classes and structs, implementing ICloneable. The Clone() method performs a shallow copy, while DeepClone() recursively deep clones reference-type properties that are also marked with [GenerateCloneable], and creates independent copies of collection properties (List<T>, Dictionary<K,V>, T[], ImmutableArray<T>, read-only collections).

Attribute

[GenerateCloneable(params string[] excludeProperties)]

Parameters:

Supported Types:

Collection Deep Cloning:

The following collection types are automatically deep copied during DeepClone():

Example

using 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; }
    public List<string>? Tags { get; set; }
    public Dictionary<string, int>? Scores { 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; }
}

// Struct support
[GenerateCloneable]
public partial struct Coordinate
{
    public double X { get; set; }
    public double Y { get; set; }
    public List<string>? Labels { get; set; }
}

Generated Code

The generator creates the following members on the partial class or struct:

Usage Example

var original = new UserProfile
{
    Id = 1,
    Name = "John Doe",
    Score = 95.5,
    Address = new Address { Street = "123 Main St", City = "Springfield" },
    Tags = new List<string> { "admin", "user" },
    Scores = new Dictionary<string, int> { ["math"] = 95, ["science"] = 88 }
};

// Shallow clone - Address and collections are the same reference
UserProfile shallow = original.Clone();
shallow.Address.City = "Shelbyville";
Console.WriteLine(original.Address.City); // "Shelbyville" (shared reference)

// Deep clone - Address and collections are new independent instances
UserProfile deep = original.DeepClone();
deep.Address.City = "Capital City";
deep.Tags.Add("editor");
deep.Scores["art"] = 72;
Console.WriteLine(original.Address.City); // "Shelbyville" (unaffected)
Console.WriteLine(original.Tags.Count);   // 2 (unaffected)
Console.WriteLine(original.Scores.Count); // 2 (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

// Struct cloning
var coord = new Coordinate { X = 1.0, Y = 2.0, Labels = new List<string> { "origin" } };
Coordinate clonedCoord = coord.DeepClone();
clonedCoord.Labels.Add("copy");
Console.WriteLine(coord.Labels.Count); // 1 (unaffected)

10. Assembly Information Generator

Generates compile-time constant properties for assembly metadata (title, version, company, copyright, etc.), eliminating the need for runtime reflection via Assembly.GetCustomAttribute<T>().

Attribute

[GenerateAssemblyInformation]

Example

using BB84.SourceGenerators.Attributes;

[GenerateAssemblyInformation]
public partial class AppInfo
{ }

Generated Code

The generator creates public const string properties for each standard assembly attribute:

Usage Example

Console.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"

11. Singleton Generator

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.

Attribute

[GenerateSingleton(bool useLazy = true)]

Parameters:

Constraints:

Example

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 { }

Generated Code (Lazy, no interface)

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() { }
}

Generated Code (Lazy, with interface)

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() { }
}

Generated Code (Non-lazy, no interface)

public partial class MyCache
{
    /// <summary>
    /// Gets the singleton instance of <see cref="MyCache"/>.
    /// </summary>
    public static MyCache Instance { get; } = new MyCache();

    private MyCache() { }
}

Usage Example

// 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

12. Decorator Generator

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.

Attribute

[GenerateDecorator]

Features

Example

using BB84.SourceGenerators.Attributes;

public interface IMyService
{
    string? Name { get; set; }
    int Count { get; }
    event EventHandler? Changed;
    string GetMessage(string input);
    T Transform<T>(T input) where T : class;
    void DoWork();
}

[GenerateDecorator]
public partial class MyServiceDecorator : IMyService
{ }

Generated Code

The generator creates the following members on the partial class:

Usage Example

// 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")

// Generic methods work seamlessly
string result = decorator.Transform("hello"); // delegates to inner.Transform<string>("hello")

// Events are delegated
decorator.Changed += (s, e) => Console.WriteLine("Changed!");

// 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;
    }
}

// Use partial method hooks for logging/tracing without subclassing
[GenerateDecorator]
public partial class TracedServiceDecorator : IMyService
{
    partial void OnGetMessageExecuting()
        => Console.WriteLine("GetMessage is about to be called");

    partial void OnGetMessageExecuted()
        => Console.WriteLine("GetMessage has completed");

    partial void OnDoWorkExecuting()
        => Trace.TraceInformation("DoWork starting");

    partial void OnDoWorkExecuted()
        => Trace.TraceInformation("DoWork completed");
}

// 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>()));

13. Factory Generator

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.

Attributes

[GenerateFactory(Type interfaceType)]
[GenerateFactoryKey(string key)]

Parameters:

Constraints:

Example

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
{ }

Generated Code

The generator creates the following static methods on the partial class:

Usage Example

// 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"

14. Disposable Generator

Generates the complete dispose pattern for classes, implementing IDisposable and optionally IAsyncDisposable. Fields marked with [DisposeResource] are automatically disposed with configurable ordering.

Attributes

[GenerateDisposable(bool generateFinalizer = false, bool async = false)]
[DisposeResource(int order = 0)]

Parameters:

Constraints:

Example

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;
    }
}

Generated Code

The generator creates:

Usage Example

// 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;
}

15. AutoMapper Generator

Generates property-to-property mapping implementations for partial methods at compile time. Decorated methods automatically map matching properties from the source parameter type to the return type, eliminating manual mapping boilerplate and runtime reflection.

Attributes

[GenerateAutoMapper]
[PropertyMapping(string sourceProperty, string targetProperty)]

Parameters:

Constraints:

Example

using BB84.SourceGenerators.Attributes;

public class UserEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public DateTime CreatedAt { get; set; }
}

public class UserDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public string DisplayName { get; set; }
}

public static partial class UserMapper
{
    [GenerateAutoMapper]
    [PropertyMapping("Name", "DisplayName")]
    public static partial UserDto ToDto(UserEntity entity);
}

Generated Code

The generator creates a mapping implementation that:

Usage Example

var entity = new UserEntity
{
    Id = 1,
    Name = "John Doe",
    Email = "john@example.com",
    CreatedAt = DateTime.UtcNow
};

// Generated method maps properties automatically
UserDto dto = UserMapper.ToDto(entity);

Console.WriteLine(dto.Id);          // 1
Console.WriteLine(dto.Name);        // "John Doe"
Console.WriteLine(dto.Email);       // "john@example.com"
Console.WriteLine(dto.DisplayName); // "John Doe" (mapped from Name)

// Works with instance methods too
public partial class OrderMapper
{
    [GenerateAutoMapper]
    public partial OrderDto MapOrder(OrderEntity order);
}

// Nullable source parameter generates null check
public static partial class SafeMapper
{
    [GenerateAutoMapper]
    public static partial UserDto ToDto(UserEntity? entity);
    // Throws ArgumentNullException if entity is null
}

Requirements

Performance Benefits

Enum Extensions

The generated enum extension methods provide significant performance improvements over reflection-based Enum methods:

Notification Properties

INI File Serialization

Builder Pattern

ToString Generation

Validation

Equality

Cloneable

Assembly Information

Singleton

Decorator

Factory

Disposable

AutoMapper

How It Works

Source generators run during compilation and generate additional C# source files that are compiled alongside your code. This means:

Developer Reference

SourceBuilder

SourceBuilder is an internal helper used by all generators to emit indented C# source text. Each Open* method appends the declaration line and a {, increments the indent level, and returns this for chaining. Each Close* method is a semantic alias for CloseBrace().

Method Emitted declaration
OpenNamespace("My.NS") namespace My.NS
OpenClass("public", "Foo") public partial class Foo
OpenClass("public", "Foo", "IDisposable") public partial class Foo : IDisposable
OpenSealedClass("public", "Foo") public sealed class Foo
OpenSealedClass("public", "Foo", "IDisposable") public sealed class Foo : IDisposable
OpenStruct("public", "Foo") public partial struct Foo
OpenStruct("public", "Foo", "IEquatable<Foo>") public partial struct Foo : IEquatable<Foo>
OpenRecord("public", "Bar") public partial record Bar
OpenRecord("public", "Bar", "IDisposable") public partial record Bar : IDisposable
OpenSealedRecord("public", "Bar") public sealed partial record Bar
OpenSealedRecord("public", "Bar", "BaseRecord") public sealed partial record Bar : BaseRecord
OpenRecordStruct("public", "Baz") public partial record struct Baz
OpenRecordStruct("public", "Baz", "IEquatable<Baz>") public partial record struct Baz : IEquatable<Baz>

Close methods: CloseNamespace(), CloseClass(), CloseStruct(), CloseRecord(), CloseRecordStruct() — all delegate to CloseBrace().

Contributing

Contributions are welcome! If you have an idea for a new feature, improvement, or bug fix, please follow these steps:

  1. Have a look at the Issues to see if your idea has already been discussed.
  2. If you want to work on an existing issue, please comment on the issue to let others know you’re working on it.
  3. Fork the repository and create a new branch for your contribution.
  4. Make your changes and commit them with clear and descriptive messages.
  5. Push your changes to your forked repository and submit a pull request to the main repository.

Code of Conduct

We expect all contributors to adhere to the Code of Conduct.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Author

Robert Peter Meyer (BoBoBaSs84)

Changelog

See releases for version history and changelog.