FractalDataWorks.SourceGenerators
0.4.0-preview.6
dotnet add package FractalDataWorks.SourceGenerators --version 0.4.0-preview.6
NuGet\Install-Package FractalDataWorks.SourceGenerators -Version 0.4.0-preview.6
<PackageReference Include="FractalDataWorks.SourceGenerators" Version="0.4.0-preview.6" />
<PackageVersion Include="FractalDataWorks.SourceGenerators" Version="0.4.0-preview.6" />
<PackageReference Include="FractalDataWorks.SourceGenerators" />
paket add FractalDataWorks.SourceGenerators --version 0.4.0-preview.6
#r "nuget: FractalDataWorks.SourceGenerators, 0.4.0-preview.6"
#:package FractalDataWorks.SourceGenerators@0.4.0-preview.6
#addin nuget:?package=FractalDataWorks.SourceGenerators&version=0.4.0-preview.6&prerelease
#tool nuget:?package=FractalDataWorks.SourceGenerators&version=0.4.0-preview.6&prerelease
FractalDataWorks.SourceGenerators
Core infrastructure for building source generators in the FractalDataWorks framework. This package provides reusable builders, generators, and models that form the foundation for all specialized source generator implementations (TypeCollections, ServiceTypes, Messages).
Overview
FractalDataWorks.SourceGenerators implements the Gang of Four Builder pattern to orchestrate complex code generation scenarios. It provides a modular architecture where specialized generators handle specific concerns (fields, methods, constructors, properties) and a director coordinates their execution.
Target Framework
netstandard2.0- Required for Roslyn source generator compatibility
Why netstandard2.0 Only (No Multi-Targeting)
Source generator projects MUST target only netstandard2.0. Multi-targeting is not supported for source generators.
Correct:
<TargetFramework>netstandard2.0</TargetFramework>
INCORRECT (will fail):
<TargetFrameworks>netstandard2.0;net8.0;net10.0</TargetFrameworks>
Why Single-Target Only?
Roslyn Host Constraint - The C# compiler (Roslyn) loads source generators into its own process, which expects netstandard2.0 assemblies. Multi-targeting would create multiple output folders (
netstandard2.0/,net8.0/,net10.0/) and Roslyn wouldn't know which to load.Analyzer Loading - When referenced as an analyzer:
<ProjectReference Include="..." OutputItemType="Analyzer" ReferenceOutputAssembly="false" />The build system specifically looks for the analyzer DLL in the netstandard2.0 output path. Multi-targeting breaks this lookup.
Package Structure - Source generators are packaged in
analyzers/dotnet/cs/directory, not the standardlib/directory. Multi-targeting doesn't apply to analyzers.
Compatibility
Despite targeting netstandard2.0, source generators work with all .NET versions:
- ✅ .NET Framework 4.7.2+
- ✅ .NET Core 3.1+
- ✅ .NET 5, 6, 7, 8
- ✅ .NET 10.0+
The generated code adapts to the consumer's target framework, not the generator's target framework.
Framework's Directory.Build.props Configuration
The FractalDataWorks framework uses Directory.Build.props to configure common settings for all projects. Note that multi-targeting is NOT automatically applied; each project specifies its own target framework.
From Directory.Build.props:
<PropertyGroup>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<Configurations>Debug;Release;Experimental;Alpha;Beta;Preview;Refactor;Test</Configurations>
</PropertyGroup>
How It Works
Project-Specific Targeting:
- Each project explicitly sets its
<TargetFramework>or<TargetFrameworks>in its.csproj - Source generators and abstractions typically use
netstandard2.0only - Services and applications typically use
net10.0only
Override Examples:
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0;net10.0</TargetFrameworks>
</PropertyGroup>
Polyfills for netstandard2.0
The framework automatically adds polyfill packages for modern C# features on netstandard2.0.
From Directory.Build.props:147-152:
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="IsExternalInit" PrivateAssets="all">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="PolySharp" PrivateAssets="all" />
</ItemGroup>
What This Enables:
initaccessors on propertiesrecordtypesrequiredmembers (with PolySharp)- Pattern matching enhancements
- Other modern C# syntax on older frameworks
Multi-Targeting for Runtime Libraries
While source generators must be netstandard2.0 only, runtime libraries in the framework use the Directory.Build.props defaults (netstandard2.0;net10.0) unless overridden:
Abstractions Pattern
Abstractions projects typically target netstandard2.0 only for maximum compatibility:
<TargetFramework>netstandard2.0</TargetFramework>
Why: Abstractions define contracts (interfaces, base classes, attributes). These are referenced by both:
- Source generators (require netstandard2.0)
- Implementation projects (may target newer frameworks)
Using netstandard2.0 ensures source generators can reference the abstractions.
Implementation Pattern
Implementation projects may use multi-targeting to leverage modern features:
<TargetFrameworks>netstandard2.0;net8.0;net10.0</TargetFrameworks>
Why: Implementations can optimize for newer frameworks while maintaining backwards compatibility:
#if NET8_0_OR_GREATER
// Use .NET 8 FrozenDictionary with alternate key lookup
private static readonly FrozenDictionary<int, IMyType> _byId = values.ToFrozenDictionary(v => v.Id);
#else
// Fall back to Dictionary for netstandard2.0
private static readonly Dictionary<int, IMyType> _byId = new(values);
#endif
Service/Application Pattern
Service and application projects target specific modern frameworks:
<TargetFramework>net10.0</TargetFramework>
Why: These are not libraries meant to be consumed by source generators. They can use the latest language features and APIs.
Multi-Targeting Decision Matrix
| Project Type | Target Framework(s) | Reasoning |
|---|---|---|
| Source Generators | netstandard2.0 |
Roslyn requirement, single target only |
| Abstractions | netstandard2.0 |
Maximum compatibility, referenced by generators |
| Analyzers | netstandard2.0 |
Roslyn requirement, single target only |
| CodeFixes | netstandard2.0 |
Roslyn requirement, single target only |
| Runtime Libraries | netstandard2.0;net8.0;net10.0 |
Optimization + compatibility |
| Services | net10.0 |
Latest features, not consumed by generators |
| Applications | net10.0 |
Latest features, final executable |
Multi-Targeting Example
Here's a complete example showing proper multi-targeting for a runtime library:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0;net10.0</TargetFrameworks>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FractalDataWorks.Results" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.1" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0' OR '$(TargetFramework)' == 'net10.0'">
</ItemGroup>
</Project>
Conditional Compilation Symbols
When multi-targeting, use conditional compilation to optimize per framework. The generated code from TypeCollection source generators uses this pattern to select optimal dictionary implementations:
// Illustrative pattern - actual implementation is in generated code
#if NET8_0_OR_GREATER
// Use modern FrozenDictionary for optimized immutable lookups
private static readonly FrozenDictionary<int, TInterface> _all = values.ToFrozenDictionary(v => v.Id);
#else
// Fall back to Dictionary for netstandard2.0
private static readonly Dictionary<int, TInterface> _all = values.ToDictionary(v => v.Id);
#endif
See GenericCollectionBuilder.Build() for the actual generation logic that produces framework-specific code.
Common Multi-Targeting Symbols
| Symbol | Target Frameworks | Use Case |
|---|---|---|
NETSTANDARD2_0 |
netstandard2.0 | Legacy compatibility code |
NET8_0_OR_GREATER |
.NET 8.0+ | FrozenDictionary, required members |
NET10_0_OR_GREATER |
.NET 10.0+ | Latest C# 13 features |
NETCOREAPP |
.NET Core 3.1+ | Core-specific APIs |
Build Output Structure
Multi-targeting creates separate build outputs:
bin/
Release/
netstandard2.0/
MyLibrary.dll
net8.0/
MyLibrary.dll
net10.0/
MyLibrary.dll
NuGet packages include all targets, and the .NET SDK automatically selects the best match for each consumer.
Progressive Build Configurations
The framework defines 6 progressive build configurations in Directory.Build.props, each with increasing quality enforcement:
| Configuration | Optimize | Analyzers | Warnings as Errors | Use Case |
|---|---|---|---|---|
| Debug | ❌ No | ❌ Disabled | ❌ No | Fast iteration, debugging |
| Experimental | ❌ No | ✅ Minimal | ❌ No | Trying new features |
| Alpha | ✅ Yes | ✅ Minimal | ❌ No | Basic validation with optimization |
| Beta | ✅ Yes | ✅ Full | ✅ Yes | Pre-release quality gate |
| Preview | ✅ Yes | ✅ Full | ✅ Yes | Release candidate |
| Release | ✅ Yes | ✅ Full | ✅ Yes | Production ready |
Configuration Details
Debug Configuration:
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<Optimize>false</Optimize>
<RunAnalyzers>false</RunAnalyzers>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
- Fast builds for rapid development
- No code analysis overhead
- Warnings don't block builds
Experimental Configuration:
<PropertyGroup Condition="'$(Configuration)' == 'Experimental'">
<Optimize>false</Optimize>
<AnalysisLevel>latest-minimum</AnalysisLevel>
<RunAnalyzers>true</RunAnalyzers>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
- Minimal analyzer enforcement
- Helps catch obvious issues early
- Still allows experimentation
Alpha Configuration:
<PropertyGroup Condition="'$(Configuration)' == 'Alpha'">
<Optimize>true</Optimize>
<AnalysisLevel>latest-minimum</AnalysisLevel>
<RunAnalyzers>true</RunAnalyzers>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
- Optimized builds with basic analysis
- Good for performance testing
- Warnings reported but don't fail build
Beta Configuration:
<PropertyGroup Condition="'$(Configuration)' == 'Beta'">
<Optimize>true</Optimize>
<AnalysisLevel>latest-recommended</AnalysisLevel>
<RunAnalyzers>true</RunAnalyzers>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
- First configuration that fails on warnings
- Full analyzer suite enabled
- Code style enforcement
- Use before committing production code
Release Configuration:
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<Optimize>true</Optimize>
<AnalysisLevel>latest-recommended</AnalysisLevel>
<RunAnalyzers>true</RunAnalyzers>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
- Production-ready builds
- Maximum optimization
- Full validation
- Required for NuGet package publishing
Analyzer Packages
All projects (except samples and tests) automatically get these analyzers via Directory.Build.props.
From Directory.Build.props:186-192:
<ItemGroup Condition="'$(IsTestProject)' != 'true' AND !$(MSBuildProjectDirectory.Contains('\samples\'))">
<PackageReference Include="AsyncFixer" PrivateAssets="All" />
<PackageReference Include="Meziantou.Analyzer" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" PrivateAssets="All" />
<PackageReference Include="Roslynator.Analyzers" PrivateAssets="All" />
<PackageReference Include="SecurityCodeScan.VS2019" PrivateAssets="All" />
</ItemGroup>
Coverage:
- AsyncFixer - Async/await best practices
- Meziantou.Analyzer - General C# best practices
- Microsoft.VisualStudio.Threading.Analyzers - Threading and async patterns
- Roslynator.Analyzers - Code quality and refactoring suggestions
- SecurityCodeScan - Security vulnerability detection
Globally Suppressed Warnings
Some warnings are globally suppressed as design decisions.
From Directory.Build.props:49:
<NoWarn>$(NoWarn);VSTHRD200;CA1716;CA1000;CA1848;CA1510;CA1834;MA0051;MA0048;CA1711;CA1707;NETSDK1195</NoWarn>
| Warning | Description | Why Suppressed |
|---|---|---|
| VSTHRD200 | Use Async naming convention | Framework uses Task suffix strategy |
| CA1716 | Identifiers should not match keywords | Acceptable for domain terms |
| CA1000 | Do not declare static members on generic types | Required for TypeCollections |
| CA1848 | Use LoggerMessage delegates | Framework uses MessageLogging instead |
| CA1510 | Use ArgumentNullException.ThrowIfNull | netstandard2.0 doesn't have it |
| CA1834 | Use StringBuilder.Append(char) | Negligible performance impact |
| MA0051 | Method too long | Acceptable for generators |
| MA0048 | File name must match type name | Generic arity variations together |
| CA1711 | Identifiers should not have incorrect suffix | Design choice for "Type" suffix |
| CA1707 | Underscores in member names | Allows _camelCase for private/protected fields |
| NETSDK1195 | SDK version warnings | Suppressed for preview SDK compatibility |
Build Command Examples
# Rapid development
dotnet build
# Test with optimization
dotnet build -c Alpha
# Pre-commit validation (fails on warnings)
dotnet build -c Beta
# Production build
dotnet build -c Release
# Build all configurations
dotnet build -c Debug && dotnet build -c Beta && dotnet build -c Release
Recommendation: Progressive Development Workflow
- Day-to-day development - Use Debug configuration
- Before committing - Run Beta build to catch issues
- Before PR - Ensure Release build succeeds
- CI/CD pipeline - Always use Release configuration
This progressive approach lets you move fast during development while ensuring production quality before release.
Architecture
Builder Pattern (GoF)
The package implements the classic Builder pattern with two key participants:
- Builder (
IGenericCollectionBuilder,GenericCollectionBuilder) - Constructs collection classes step-by-step - Director (
GenericCollectionDirector) - Orchestrates the build process and determines generation strategies
Specialized Generators
Individual generators follow the Single Responsibility Principle:
EmptyClassGenerator- Generates empty/not-found implementationsFieldGenerator- Generates static fields and lookup dictionariesLookupMethodGenerator- Generates lookup methods (ById, ByName, etc.)StaticConstructorGenerator- Generates static constructors with initialization logicValuePropertyGenerator- Generates static properties for collection values
Configuration
CollectionBuilderConfiguration - Defines collection-specific settings.
From CollectionBuilderConfiguration.cs:9-46:
public sealed class CollectionBuilderConfiguration
{
/// <summary>
/// Gets the fully qualified name of the base collection class (e.g., "FractalDataWorks.Collections.TypeCollectionBase").
/// </summary>
public string BaseCollectionTypeName { get; init; } = string.Empty;
/// <summary>
/// Gets the generic arity of the base collection type (e.g., 1 for TypeCollectionBase<T>, 5 for ServiceTypeCollectionBase<T,T1,T2,T3,T4>).
/// </summary>
public int BaseCollectionArity { get; init; }
/// <summary>
/// Gets the namespace containing the base collection types (e.g., "FractalDataWorks.Collections").
/// </summary>
public string BaseNamespace { get; init; } = string.Empty;
/// <summary>
/// Gets the name of the attribute that marks collection types (e.g., "TypeCollection", "ServiceTypeCollection").
/// </summary>
public string CollectionAttributeName { get; init; } = string.Empty;
/// <summary>
/// Gets the name of the attribute that marks collection value/option types (e.g., "TypeOption", "ServiceTypeOption").
/// </summary>
public string ValueAttributeName { get; init; } = string.Empty;
/// <summary>
/// Creates a configuration for TypeCollections (FractalDataWorks.Collections).
/// </summary>
public static CollectionBuilderConfiguration ForTypeCollections() => new()
{
BaseCollectionTypeName = "FractalDataWorks.Collections.TypeCollectionBase",
BaseCollectionArity = 1,
BaseNamespace = "FractalDataWorks.Collections",
CollectionAttributeName = "TypeCollection",
ValueAttributeName = "TypeOption"
};
/// <summary>
/// Creates a configuration for ServiceTypeCollections (FractalDataWorks.ServiceTypes).
/// </summary>
public static CollectionBuilderConfiguration ForServiceTypeCollections() => new()
{
BaseCollectionTypeName = "FractalDataWorks.ServiceTypes.ServiceTypeCollectionBase",
BaseCollectionArity = 5,
BaseNamespace = "FractalDataWorks.ServiceTypes",
CollectionAttributeName = "ServiceTypeCollection",
ValueAttributeName = "ServiceTypeOption"
};
}
Core Components
IGenericCollectionBuilder
Fluent builder interface for constructing collection classes. The interface is generic to support different ID types (int for Collections, Guid for ServiceTypes).
From IGenericCollectionBuilder.cs:14-91:
/// <typeparam name="TId">The type used for collection IDs (int for Collections, Guid for ServiceTypes).</typeparam>
public interface IGenericCollectionBuilder<TId>
where TId : struct
{
IGenericCollectionBuilder<TId> Configure(CollectionGenerationMode mode);
IGenericCollectionBuilder<TId> WithDefinition(GenericTypeInfoModel<TId> definition);
IGenericCollectionBuilder<TId> WithValues(IList<GenericValueInfoModel<TId>> values);
IGenericCollectionBuilder<TId> WithReturnType(string returnType);
IGenericCollectionBuilder<TId> WithCompilation(Compilation compilation);
IGenericCollectionBuilder<TId> WithUserClassModifiers(bool isStatic, bool isAbstract);
string Build();
}
/// <summary>
/// Non-generic type alias for IGenericCollectionBuilder using int IDs.
/// Provides backward compatibility for Collections and Messages generators.
/// </summary>
public interface IGenericCollectionBuilder : IGenericCollectionBuilder<int>
{
}
GenericCollectionDirector
Orchestrates the building process and determines generation strategies. The director is generic to match the builder interface.
From GenericCollectionDirector.cs:15-60:
/// <typeparam name="TId">The type used for collection IDs (int for Collections, Guid for ServiceTypes).</typeparam>
public sealed class GenericCollectionDirector<TId>
where TId : struct
{
private readonly IGenericCollectionBuilder<TId> _builder;
public GenericCollectionDirector(IGenericCollectionBuilder<TId> builder)
{
_builder = builder ?? throw new ArgumentNullException(nameof(builder));
}
public string ConstructFullCollection(
GenericTypeInfoModel<TId> definition,
IList<GenericValueInfoModel<TId>> values,
string returnType,
Compilation compilation)
{
ValidateParameters(definition, values, returnType, compilation);
var mode = DetermineGenerationMode(definition);
return _builder
.Configure(mode)
.WithDefinition(definition)
.WithValues(values)
.WithReturnType(returnType)
.WithCompilation(compilation)
.Build();
}
public string ConstructSimplifiedCollection(
GenericTypeInfoModel<TId> definition,
IList<GenericValueInfoModel<TId>> values,
string returnType,
Compilation compilation);
}
The director automatically determines the appropriate CollectionGenerationMode based on the CollectionStrategy property.
From CollectionGenerationMode.cs:10-42:
StaticCollection- Static class with static members (default for read-only collections)InstanceCollection- Singleton pattern with instance membersFactoryCollection- Factory methods for creating instances on demandServiceCollection- Designed for dependency injection scenarios
GenericCollectionBuilder
Concrete implementation that orchestrates specialized generators. The builder initializes and coordinates the individual generators.
From GenericCollectionBuilder.cs:19-52:
public sealed class GenericCollectionBuilder<TId> : IGenericCollectionBuilder<TId>
where TId : struct
{
private readonly CollectionBuilderConfiguration _config;
private readonly FieldGenerator<TId> _fieldGenerator;
private readonly LookupMethodGenerator _lookupMethodGenerator;
private readonly EmptyClassGenerator _emptyClassGenerator;
private readonly StaticConstructorGenerator<TId> _staticConstructorGenerator;
private readonly ValuePropertyGenerator<TId> _valuePropertyGenerator;
public GenericCollectionBuilder(CollectionBuilderConfiguration config)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
_config.Validate();
// Initialize generators
_fieldGenerator = new FieldGenerator<TId>(config);
_lookupMethodGenerator = new LookupMethodGenerator(config);
_emptyClassGenerator = new EmptyClassGenerator(config);
_staticConstructorGenerator = new StaticConstructorGenerator<TId>(config);
_valuePropertyGenerator = new ValuePropertyGenerator<TId>(config);
}
// ... fluent methods and Build()
}
Usage example (for int-based TypeCollections):
var config = CollectionBuilderConfiguration.ForTypeCollections();
var builder = new GenericCollectionBuilder<int>(config);
var director = new GenericCollectionDirector<int>(builder);
var generatedCode = director.ConstructFullCollection(
definition: typeInfoModel,
values: valueList,
returnType: "IMyType",
compilation: compilation);
Models
GenericTypeInfoModel
Metadata about the collection type being generated. This is a generic wrapper around CollectionTypeInfoModel<TId>.
From CollectionTypeInfoModel.cs:14-80:
public class CollectionTypeInfoModel<TId> : IInputInfoModel, IEquatable<CollectionTypeInfoModel<TId>>
where TId : struct
{
public string Namespace { get; set; } = string.Empty;
public string ClassName { get; set; } = string.Empty;
public string FullTypeName { get; set; } = string.Empty;
public bool IsGenericType { get; set; }
public string CollectionName { get; set; } = string.Empty;
public string? CollectionBaseType { get; set; }
public bool GenerateFactoryMethods { get; set; } = true;
public bool GenerateStaticCollection { get; set; } = true;
public bool Generic { get; set; }
public string Strategy { get; set; } = "Default";
public CollectionStrategy CollectionStrategy { get; set; } = CollectionStrategy.Immutable;
public StringComparison NameComparison { get; set; } = StringComparison.OrdinalIgnoreCase;
public bool IncludeReferencedAssemblies { get; set; }
public bool UseMethods { get; set; }
public bool UseDictionaryStorage { get; set; } = true;
public string? KeyType { get; set; }
public bool IsParentCollection { get; set; }
public string? MemberOfParent { get; set; }
public string? ReturnType { get; set; }
public bool InheritsFromCollectionBase { get; set; }
public EquatableArray<PropertyLookupInfoModel> LookupProperties { get; set; }
public EquatableArray<CollectionValueInfoModel<TId>> ConcreteTypes { get; set; }
// ... additional properties for generic type handling
}
GenericValueInfoModel
Metadata about individual collection values/options.
From CollectionValueInfoModel.cs:14-95:
public class CollectionValueInfoModel<TId> : IInputInfoModel, IEquatable<CollectionValueInfoModel<TId>>
where TId : struct
{
public string FullTypeName { get; set; } = string.Empty;
public string ShortTypeName { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public bool Include { get; set; } = true;
public int Order { get; set; }
public string? Description { get; set; }
public bool IsAbstract { get; set; }
public bool IsStatic { get; set; }
public bool IsGenericType { get; set; }
public IDictionary<string, string> Properties { get; }
public ISet<string> Categories { get; }
public string? ReturnType { get; set; }
public string? ReturnTypeNamespace { get; set; }
public bool? GenerateFactoryMethod { get; set; }
public IList<ConstructorInfo> Constructors { get; set; } = new List<ConstructorInfo>();
public TId? BaseConstructorId { get; set; }
}
Non-generic aliases are provided for backward compatibility:
public class GenericTypeInfoModel : GenericTypeInfoModel<int> { }
public class GenericValueInfoModel : GenericValueInfoModel<int> { }
Generation Process
Step-by-Step Flow
- Configuration - Create
CollectionBuilderConfigurationwith collection-specific settings - Model Creation - Build
GenericTypeInfoModelandList<GenericValueInfoModel>from source analysis - Director Setup - Create director with configured builder
- Generation - Call
ConstructFullCollectionorConstructSimplifiedCollection - Output - Receive complete C# source code as string
Internal Build Process
When Build() is called:
- Class Declaration - Creates partial class with proper modifiers
- Using Directives - Collects and deduplicates namespaces from all types
- Fields - Generates
_all(FrozenDictionary),_empty, and lookup dictionaries - Static Constructor - Initializes all static fields
- Lookup Methods - Generates
ById(),ByName(), custom lookups - Value Properties - Generates static properties for each value
- Helper Methods - Generates
All(),NotFound()methods
Generated Code Structure
The builder generates a partial class with the following structure. Key generation logic is in GenericCollectionBuilder.Build().
From GenericCollectionBuilder.cs:127-347:
The generated code includes:
- Using directives - Collects namespaces from all value types and adds required system namespaces
- Static fields -
_all(FrozenDictionary for Immutable strategy, ConcurrentDictionary for Mutable),_empty, and lookup dictionaries - Static constructor - Initializes all fields via
StaticConstructorGenerator - Lookup methods - Generated via
LookupMethodGeneratorbased onLookupProperties All()method - Returns_all.Values.ToList()NotFound()method - Returns_emptyRegister()method - For Mutable and Factory strategies only- Value properties - Static properties for each discovered value via
ValuePropertyGenerator
See the FractalDataWorks.Collections.SourceGenerators package for actual generated output examples.
Helper Services
GenericTypeHelper
Utilities for working with generic types in Roslyn. Handles type arity detection, metadata format conversion, and generic type parameter extraction.
From GenericTypeHelper.cs:13-200:
public static class GenericTypeHelper
{
/// <summary>
/// Checks if a type is generic (has type parameters).
/// </summary>
public static bool IsGenericType(INamedTypeSymbol? typeSymbol);
/// <summary>
/// Gets the arity (number of type parameters) for a type.
/// Returns 0 for non-generic types.
/// </summary>
public static int GetArity(INamedTypeSymbol? typeSymbol);
/// <summary>
/// Gets the metadata name for a type (e.g., "Type`2" for Type<T1,T2>).
/// This is the format required by Compilation.GetTypeByMetadataName().
/// </summary>
public static string GetMetadataName(INamedTypeSymbol typeSymbol);
/// <summary>
/// Gets the fully qualified metadata name including namespace (e.g., "Namespace.Type`2").
/// </summary>
public static string GetFullMetadataName(INamedTypeSymbol typeSymbol);
/// <summary>
/// Gets the type parameter list as a string (e.g., "<T1, T2, T3>").
/// </summary>
public static string GetTypeParameterList(INamedTypeSymbol typeSymbol);
/// <summary>
/// Gets the type parameter constraints for class declarations.
/// </summary>
public static string GetTypeParameterConstraints(INamedTypeSymbol typeSymbol, string indent = " ");
}
AttributeBasedGeneratorHelper
Helper for creating ForAttributeWithMetadataName-based incremental generators with optimized attribute discovery.
From AttributeBasedGeneratorHelper.cs:16-138:
public static class AttributeBasedGeneratorHelper
{
/// <summary>
/// Creates an optimized provider for discovering types with a specific attribute.
/// Uses ForAttributeWithMetadataName for faster discovery than manual scanning.
/// </summary>
public static IncrementalValuesProvider<(INamedTypeSymbol? TypeSymbol, AttributeData? Attribute)>
CreateAttributeProvider(
IncrementalGeneratorInitializationContext context,
string attributeFullName,
Func<SyntaxNode, CancellationToken, bool>? predicate = null);
/// <summary>
/// Filters option types that belong to a specific collection.
/// </summary>
public static IReadOnlyList<INamedTypeSymbol> FilterRelevantOptions(...);
/// <summary>
/// Checks if a collection should generate code (only in origin assembly).
/// </summary>
public static bool ShouldGenerateForCollection(...);
/// <summary>
/// Extracts a Type argument from an attribute's constructor at a specific index.
/// </summary>
public static INamedTypeSymbol? ExtractTypeArgument(AttributeData attribute, int index);
}
Performance Characteristics
- Compile-Time Generation - All code generated during compilation (zero runtime reflection)
- FrozenDictionary Lookups - O(1) lookup performance for Immutable strategy (uses
System.Collections.Frozen.FrozenDictionary) - ConcurrentDictionary - Thread-safe O(1) lookups for Mutable strategy
- Minimal Allocations - Static fields initialized once in static constructor
- String Comparisons - Configurable via
NameComparisonproperty (default:OrdinalIgnoreCase)
Usage by Specialized Generators
This package serves as the foundation for:
- FractalDataWorks.Collections.SourceGenerators - TypeCollection AND ServiceType collection generation
- FractalDataWorks.Messages.SourceGenerators - Message collection generation
NOTE: There is NO separate ServiceTypes.SourceGenerators - the Collections generator handles both TypeCollections AND ServiceTypes.
Each specialized generator:
- Creates appropriate
CollectionBuilderConfiguration - Collects type metadata into
GenericTypeInfoModelandGenericValueInfoModelinstances - Uses
GenericCollectionDirectorto orchestrate generation - Handles domain-specific logic (validation, diagnostics, etc.)
Windows 260-Character Path Limit Workaround
The Problem
Source generators that reference multiple NuGet packages face a critical issue on Windows: the 260-character path limit. When Roslyn loads a source generator, it creates deep nested paths for each dependency:
C:\Users\username\.nuget\packages\
microsoft.codeanalysis.csharp\5.0.0\lib\netstandard2.0\
Microsoft.CodeAnalysis.CSharp.dll
With multiple dependencies, the generated paths exceed 260 characters, causing:
- Silent failures - Generator loads but dependencies don't resolve
- Runtime exceptions - FileNotFoundException when accessing dependency types
- Inconsistent behavior - Works in some environments, fails in others
Dependency Approaches
FractalDataWorks source generators use two approaches depending on their needs:
Minimal Dependencies (Collections.SourceGenerators)
Collections.SourceGenerators uses only Roslyn APIs:
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
| Aspect | Benefit |
|---|---|
| Build time | No post-build merge step |
| Debugging | Direct source mapping works |
| Compatibility | No version conflicts possible |
| Package size | Smaller, only generator code |
| Path limits | No deep dependency chains |
ILRepack (Messages/MessageLogging.SourceGenerators)
Messages.SourceGenerators and MessageLogging.SourceGenerators use ILRepack because they depend on CodeBuilder libraries:
<ItemGroup>
<PackageReference Include="ILRepack.Lib.MSBuild.Task" PrivateAssets="all" />
<ProjectReference Include="..\FractalDataWorks.CodeBuilder.CSharp\..." PrivateAssets="all" />
</ItemGroup>
These merge dependencies post-build into a single DLL.
Dependencies
This package has the following dependencies:
From FractalDataWorks.SourceGenerators.csproj:
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.Bcl.HashCode" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FractalDataWorks.CodeBuilder.Abstractions\..." />
<ProjectReference Include="..\FractalDataWorks.CodeBuilder.CSharp\..." />
</ItemGroup>
- Microsoft.CodeAnalysis.CSharp - Roslyn API for source generation
- Microsoft.CodeAnalysis.Analyzers - Analyzer SDK
- Microsoft.Bcl.HashCode - HashCode support for netstandard2.0
- FractalDataWorks.CodeBuilder.Abstractions - Fluent code builder interfaces
- FractalDataWorks.CodeBuilder.CSharp - C# code builder implementation
| Generator | Additional Dependencies | Approach |
|---|---|---|
| Collections.SourceGenerators | None beyond Roslyn | Minimal dependencies |
| Messages.SourceGenerators | CodeBuilder, Messages | ILRepack merge |
| MessageLogging.SourceGenerators | CodeBuilder, MessageLogging | ILRepack merge |
Integration Example
Example pattern for creating a specialized source generator using this infrastructure:
// In a specialized source generator
[Generator]
public class MyCollectionGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Use AttributeBasedGeneratorHelper for optimized attribute discovery
var collectionProvider = AttributeBasedGeneratorHelper.CreateAttributeProvider(
context,
"MyNamespace.MyCollectionAttribute");
context.RegisterSourceOutput(collectionProvider, (spc, source) =>
{
// 1. Create configuration
var config = CollectionBuilderConfiguration.ForTypeCollections();
// 2. Build models from syntax analysis
var definition = BuildDefinitionModel(source); // Returns GenericTypeInfoModel<int>
var values = CollectValues(source); // Returns IList<GenericValueInfoModel<int>>
// 3. Generate code using generic builder
var builder = new GenericCollectionBuilder<int>(config);
var director = new GenericCollectionDirector<int>(builder);
var code = director.ConstructFullCollection(
definition,
values,
"IMyInterface",
spc.Compilation);
// 4. Add to compilation
spc.AddSource($"{definition.CollectionName}.g.cs", code);
});
}
}
For Guid-based ServiceTypes, use GenericCollectionBuilder<Guid> and GenericCollectionDirector<Guid>.
Design Principles
- Single Responsibility - Each generator handles one concern
- Open/Closed - Extensible through configuration, closed for modification
- Builder Pattern - Fluent API for complex object construction
- Director Pattern - Encapsulates construction logic and strategies
- Immutable Models - All models use
initaccessors - Null Safety - Nullable reference types enabled
Testing Considerations
When testing generators using this infrastructure:
- Mock
Compilationfor type resolution - Provide complete
GenericTypeInfoModelandGenericValueInfoModeldata - Verify generated code compiles using Roslyn
CSharpCompilation - Test different
CollectionGenerationModescenarios - Validate namespace collection and deduplication
Related Packages
- FractalDataWorks.CodeBuilder.Abstractions - Fluent API for code generation
- FractalDataWorks.CodeBuilder.CSharp - C# implementation of code builders
- FractalDataWorks.Collections - Runtime TypeCollection infrastructure
- FractalDataWorks.Services.Abstractions - Runtime ServiceType infrastructure
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- FractalDataWorks.CodeBuilder.Abstractions (>= 0.4.0-preview.6)
- FractalDataWorks.CodeBuilder.CSharp (>= 0.4.0-preview.6)
- Microsoft.Bcl.HashCode (>= 6.0.0)
- Microsoft.CodeAnalysis.CSharp (>= 5.0.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on FractalDataWorks.SourceGenerators:
| Package | Downloads |
|---|---|
|
FractalDataWorks.Configuration.SourceGenerators
Development tools and utilities for the FractalDataWorks ecosystem. Build: |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|