A collection of C# source generators that automatically generate boilerplate code at compile time, reducing manual coding and improving code maintainability.
This package provides fifteen powerful source generators:
Clone() and DeepClone() method generation with collection and struct supportIDisposable / 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
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>
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(string name) - Checks if an enum name is definedGetNamesFast() - Returns all enum names as an IEnumerable<string> (static method)GetValuesFast() - Returns all enum values as an IEnumerable<TEnum> (static method)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 = 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();
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)]
[AlsoNotify(params string[] propertyNames)]
[ExcludeFromNotification]
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 changesField-Level Attributes:
[AlsoNotify(...)] - Specifies additional property names that should raise change notifications when the decorated field changes. Works for both PropertyChanged and PropertyChanging events.[ExcludeFromNotification] - Excludes the decorated field from notification property generation. The field will not have a corresponding public property generated.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}";
}
The generator creates:
[ExcludeFromNotification])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)[AlsoNotify]HasChanged property (when hasChanged parameter is true)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;
}
Generates interface and implementation classes for static classes, making them testable through dependency injection.
[GenerateAbstraction(Type targetType, Type abstractionType, Type implementationType)]
Parameters:
targetType - The static class to generate an abstraction forabstractionType - The interface type to generateimplementationType - The implementation class type to generateProperties:
ExcludeMethods - Optional array of method names to exclude from generationExcludeProperties - Optional array of property 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
{ }
// Generate abstraction with exclusions
[GenerateAbstraction(typeof(Environment), typeof(IEnvironmentProvider), typeof(EnvironmentProvider),
ExcludeMethods = ["Exit", "FailFast"],
ExcludeProperties = ["ExitCode"])]
public partial class EnvironmentProvider
{ }
public partial interface IEnvironmentProvider
{ }
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 and an instance Load method for classes, enabling compile-time INI file serialization, deserialization, and instance-to-instance value copying 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, 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.
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 methods on the decorated class:
Read(string content) - Static method that parses an INI file string and returns a deserialized instanceTryRead(string content, out T? result) - Static method that attempts to parse an INI file string, returning true on success or false on failure without throwing exceptionsWrite(TClass instance) - Static method that serializes an instance into an INI file stringReadAsync(string filePath) - Static async method that reads a file line-by-line via ReadLineAsync and deserializes INI content; truly non-blocking I/OReadAsync(TextReader reader) - Static async method that reads and deserializes INI content from a TextReaderReadAsync(Stream stream) - Static async method that reads and deserializes INI content from a StreamWriteAsync(TClass instance, TextWriter writer) - Static async method that serializes an instance and writes it to a TextWriterWriteAsync(TClass instance, Stream stream) - Static async method that serializes an instance and writes it to a StreamLoad(TClass other) - Instance method that copies all section/value properties from another instance into this instance (null-safe per section)// 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
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 }. Collection properties (lists, arrays, dictionaries) are rendered with configurable formatting instead of the default type name.
[GenerateToString(params string[] excludeProperties)]
[ToStringFormat(string format)] // per-property
[ToStringOrder(int order)] // per-property
Parameters:
excludeProperties - Optional list of property names to exclude from the generated ToString() outputCollectionFormat - Controls how collection properties are rendered (named argument, default: CollectionFormat.Count)Separator - Custom separator string between properties (named argument, default: ", ")Formattable - When true, generates an IFormattable implementation so the class participates in composite formatting with custom IFormatProvider support (named argument, default: false)NullPlaceholder - When set, nullable properties with a null value render this string instead of an empty string (named argument, default: null — empty string behavior). Common values: "null", "<null>"IncludeInherited - When true, public properties from base classes (up to but not including object) are also included in the output after the declared properties. The excludeProperties list applies to inherited members as well (named argument, default: false)Per-Property Formatting:
[ToStringFormat("format")] - Applied to individual properties to specify a custom format string. The format is passed to the property’s ToString(string) method at runtime (e.g., "yyyy-MM-dd" for dates, "C2" for currency, "F2" for fixed-point).[ToStringOrder(n)] - Controls the position of a property in the generated output. Properties with an explicit order appear first in ascending order, followed by unordered properties in their natural declaration order. Can be combined with [ToStringFormat] and works alongside excludeProperties.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.
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; }
}
The generator creates a ToString() override on the partial class that:
ClassName { Prop1 = val1, Prop2 = val2 }ClassName { } when all properties are excluded[ToStringFormat], uses the format specifier in the interpolated string (e.g., {CreatedAt:yyyy-MM-dd})CollectionFormat:
Count: Count = NElements: [item1, item2] for arrays/lists, {key1: val1, key2: val2} for dictionariesTypeAndCount: TypeName (Count = N)Elements mode, emits a private generic helper DictionaryToString<TKey, TValue>Formattable = true, implements IFormattable with ToString(string?, IFormatProvider?) — properties whose types implement IFormattable receive the formatProvider; others use their default ToString()NullPlaceholder is set, nullable properties with a null value emit Property?.ToString() ?? "placeholder" instead of the raw interpolation slot; [ToStringFormat] is preserved as an explicit .ToString("fmt") call[ToStringOrder(n)], properties are reordered: those with an explicit order appear first (ascending by value), followed by unordered properties in their original declaration orderIncludeInherited = true, public readable properties from all base classes (up to but not including object) are appended after the declared properties; excludeProperties applies to inherited members as wellvar 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 }
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 pattern[EmailAddress] - Validates that a string is a valid email address format[Url] - Validates that a string is a valid fully-qualified HTTP, HTTPS, or FTP URL[Phone] - Validates that a string is a valid phone number format[CreditCard] - Validates that a string passes the Luhn algorithm (credit card check)[Compare("OtherProperty")] - Validates that the property value matches another property’s value[AllowedValues(v1, v2, ...)] (.NET 8+) - Emits an inline membership check; fails if the property value is not one of the listed values[DeniedValues(v1, v2, ...)] (.NET 8+) - Emits an inline exclusion check; fails if the property value equals any of the listed valuesValidationAttribute subclasses - Any attribute inheriting from ValidationAttribute is automatically detected and validated via IsValid() delegationIValidatableObject Integration:
When a class implements IValidatableObject, the generated Validate() method automatically calls IValidatableObject.Validate(ValidationContext) and merges the results into the error dictionary.
Constraints:
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; }
}
// 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; }
}
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 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).
[GenerateCloneable(params string[] excludeProperties)]
Parameters:
excludeProperties - Optional list of property names to exclude from the generated clone methodsSupported Types:
partial class)partial struct)Collection Deep Cloning:
The following collection types are automatically deep copied during DeepClone():
List<T> — creates a new list; if T is marked with [GenerateCloneable], elements are recursively deep clonedDictionary<TKey, TValue> — creates a new dictionary; if TValue is marked with [GenerateCloneable], values are recursively deep clonedT[] (arrays) — creates a new array copy; if T is marked with [GenerateCloneable], elements are recursively deep clonedImmutableArray<T> — value type, copied directly; if T is marked with [GenerateCloneable], elements are recursively deep cloned into a new ImmutableArray<T>ReadOnlyCollection<T>, IReadOnlyList<T>, IReadOnlyCollection<T> — creates a new read-only collection; if T is marked with [GenerateCloneable], elements are recursively deep clonedusing 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; }
}
The generator creates the following members on the partial class or struct:
Clone() — creates a shallow copy by assigning all included public read/write properties (for structs, copies the value)DeepClone() — creates a deep copy; reference-type properties marked with [GenerateCloneable] are recursively deep cloned, collection properties are independently copied, 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" },
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)
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]
add/remove accessorspartial void On{MethodName}Executing() and partial void On{MethodName}Executed() methods are generated for each interface method, enabling optional logging/tracing without subclassingusing 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
{ }
The generator creates the following members on the partial class:
_inner)add/remove accessors that delegate to the inner instancepartial void On{MethodName}Executing() and partial void On{MethodName}Executed() declarations for each method, invoked before and after delegation respectively// 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>()));
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;
}
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.
[GenerateAutoMapper]
[PropertyMapping(string sourceProperty, string targetProperty)]
Parameters:
GenerateAutoMapper - Marks a partial method for automatic mapping code generation. The method must have exactly one parameter (source) and a non-void return type (target).PropertyMapping - Specifies a custom property name mapping between source and target types. Can be applied multiple times for multiple custom mappings.Constraints:
partial and defined in a partial classusing 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);
}
The generator creates a mapping implementation that:
[PropertyMapping]ArgumentNullException guardsvar 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
}
The generated enum extension methods provide significant performance improvements over reflection-based Enum methods:
CultureInfo.InvariantCulture for consistent cross-platform formattingGuid, TimeSpan, DateTimeOffset, and collection types (List<T>, T[])TryRead for safe parsing without exceptionsReadAsync/WriteAsync for non-blocking I/O with file path (line-by-line via ReadLineAsync), Stream, and TextReader/TextWriterToString() 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 runtime[EmailAddress], [Url], [Phone], [CreditCard], and [Compare] attributesValidationAttribute subclasses and delegates to IsValid()IValidatableObject for complex cross-property validation logicEquals, 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]List<T>, Dictionary<K,V>, T[], ImmutableArray<T>, read-only collections)[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 implementationsadd/remove accessorspartial void hooks (On{Method}Executing/On{Method}Executed) for optional logging/tracing without subclassingAssembly.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)[PropertyMapping] attributeSource generators run during compilation and generate additional C# source files that are compiled alongside your code. This means:
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().
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.