Skip to content

Source Generators Specification

This is the API specification for source generators provided by the Functorium framework. All generators are based on Roslyn IIncrementalGenerator and included in the Functorium.SourceGenerators package. For practical usage, see the Source Generator Observability Tutorial.

GeneratorTrigger AttributeGeneration Target
EntityIdGenerator[GenerateEntityId]{Entity}Id struct, Comparer, Converter
ObservablePortGenerator[GenerateObservablePort]{Class}Observable wrapper class (Tracing, Logging, Metrics)
CtxEnricherGenerator(auto-detected)IUsecaseCtxEnricher implementation
DomainEventCtxEnricherGenerator(auto-detected)IDomainEventCtxEnricher implementation
UnionTypeGenerator[UnionType]Match, Switch, Is{Case}, As{Case} methods
AttributeNamespaceTargetDescription
[ObservablePortIgnore]Functorium.Adapters.SourceGeneratorsMethodExcludes the method from Observable generation
[CtxIgnore]Functorium.Applications.UsecasesClass, Property, ParameterExcluded from CtxEnricher generation
[CtxRoot]Functorium.Abstractions.ObservabilitiesInterface, Property, ParameterPromoted to ctx root level
CodeSeverityGeneratorDescription
FUNCTORIUM001ErrorObservablePortGeneratorConstructor parameter type duplication
FUNCTORIUM002WarningCtxEnricherGenerator, DomainEventCtxEnricherGeneratorctx field type conflict (OpenSearch mapping)
FUNCTORIUM003WarningCtxEnricherGeneratorRequest type inaccessible
FUNCTORIUM004WarningDomainEventCtxEnricherGeneratorEvent type inaccessible

The abstract base class for all generators. Implements IIncrementalGenerator and standardizes pipeline registration and source output.

public abstract class IncrementalGeneratorBase<TValue>(
Func<IncrementalGeneratorInitializationContext, IncrementalValuesProvider<TValue>> registerSourceProvider,
Action<SourceProductionContext, ImmutableArray<TValue>> generate,
bool AttachDebugger = false) : IIncrementalGenerator
ParameterDescription
registerSourceProviderRegisters the syntax/semantic analysis pipeline and returns IncrementalValuesProvider<TValue>
generateGenerates source files from the collected metadata array
AttachDebuggerCalls Debugger.Launch() in DEBUG builds when true

Execution flow: Initialize -> Create IncrementalValuesProvider<TValue> via registerSourceProvider -> null filtering -> Collect() -> generate call


Applying [GenerateEntityId] to an Entity class auto-generates a Ulid-based ID type, EF Core Comparer, and EF Core Converter.

Functorium.Domains.Entities
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class GenerateEntityIdAttribute : Attribute;

Target: class declarations with [GenerateEntityId] applied

Generates the following three types for the {EntityName} class in a single .g.cs file.

Generated TypeDescription
{EntityName}Idreadonly partial record struct — Ulid-based Entity ID
{EntityName}IdComparerEF Core ValueComparer<{EntityName}Id>
{EntityName}IdConverterEF Core ValueConverter<{EntityName}Id, string>
[DebuggerDisplay("{Value}")]
[JsonConverter(typeof({EntityName}IdJsonConverter))]
[TypeConverter(typeof({EntityName}IdTypeConverter))]
public readonly partial record struct {EntityName}Id :
IEntityId<{EntityName}Id>,
IParsable<{EntityName}Id>
{
public const string Name = "{EntityName}Id";
public const string Namespace = "{Namespace}";
public static readonly {EntityName}Id Empty;
public Ulid Value { get; init; }
// Factory Methods
public static {EntityName}Id New();
public static {EntityName}Id Create(Ulid id);
public static {EntityName}Id Create(string id);
// IComparable<T>
public int CompareTo({EntityName}Id other);
// IParsable<T>
public static {EntityName}Id Parse(string s, IFormatProvider? provider);
public static bool TryParse(string? s, IFormatProvider? provider, out {EntityName}Id result);
public override string ToString();
// Internal nested classes
internal sealed class {EntityName}IdJsonConverter : JsonConverter<{EntityName}Id>;
internal sealed class {EntityName}IdTypeConverter : TypeConverter;
}
public sealed class {EntityName}IdComparer : ValueComparer<{EntityName}Id>;
public sealed class {EntityName}IdConverter : ValueConverter<{EntityName}Id, string>;

EntityIdGenerator does not currently emit dedicated diagnostic codes.


Applying [GenerateObservablePort] to an Adapter class auto-generates an Observable wrapper class that provides OpenTelemetry-based Observability (Tracing, Logging, Metrics).

Functorium.Adapters.SourceGenerators
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class GenerateObservablePortAttribute : Attribute;

Target: class with [GenerateObservablePort] applied, targeting methods with FinT<IO, T> return type among methods of interfaces inheriting IObservablePort.

Exclusion condition: Methods with [ObservablePortIgnore] applied are excluded from generation.

Functorium.Adapters.SourceGenerators
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public sealed class ObservablePortIgnoreAttribute : Attribute;

Generates the following for {ClassName}.

Generated TypeDescription
{ClassName}ObservableObservable wrapper class that inherits the original class
{ClassName}ObservableLoggersLoggerMessage.Define-based high-performance logging extension method static class
public class {ClassName}Observable : {ClassName}
{
// Infrastructure fields
private readonly ActivitySource _activitySource;
private readonly ILogger<{ClassName}Observable> _logger;
private readonly Counter<long> _requestCounter;
private readonly Counter<long> _responseCounter;
private readonly Histogram<double> _durationHistogram;
// Constructor (DI parameters + parent constructor parameters)
public {ClassName}Observable(
ActivitySource activitySource,
ILogger<{ClassName}Observable> logger,
IMeterFactory meterFactory,
IOptions<OpenTelemetryOptions> openTelemetryOptions,
... /* parent constructor parameters */);
// IObservablePort interface method override
public override FinT<IO, TResult> {MethodName}(...);
}

Observability provided by each override method:

ItemContent
TracingCreates span via ActivitySource.StartActivity, records success/failure status
Logging4 levels: Request (Debug/Info), Response success (Debug/Info), Response failure (Warning/Error)
Metricsadapter.{category}.requests Counter, adapter.{category}.responses Counter, adapter.{category}.duration Histogram

Constructor parameter name conflict resolution: If the parent class constructor parameter name conflicts with reserved names(activitySource, logger, meterFactory, openTelemetryOptions), a base prefix is added. Example: logger -> baseLogger

Generates high-performance static logging methods using LoggerMessage.Define.

internal static class {ClassName}ObservableLoggers
{
// LoggerMessage.Define-based delegate fields (6 parameters or fewer)
private static readonly Action<ILogger, ...> _logAdapterRequest_{ClassName}_{MethodName};
private static readonly Action<ILogger, ...> _logAdapterRequestDebug_{ClassName}_{MethodName};
private static readonly Action<ILogger, ...> _logAdapterResponseSuccess_{ClassName}_{MethodName};
// Extension methods
public static void LogAdapterRequest_{ClassName}_{MethodName}(this ILogger logger, ...);
public static void LogAdapterRequestDebug_{ClassName}_{MethodName}(this ILogger logger, ...);
public static void LogAdapterResponseSuccessDebug_{ClassName}_{MethodName}(this ILogger logger, ...);
public static void LogAdapterResponseSuccess_{ClassName}_{MethodName}(this ILogger logger, ...);
public static void LogAdapterResponseWarning_{ClassName}_{MethodName}(this ILogger logger, ...);
public static void LogAdapterResponseError_{ClassName}_{MethodName}(this ILogger logger, ...);
}
CodeSeverityMessage
FUNCTORIUM001ErrorObservable constructor for '{ClassName}' contains multiple parameters of the same type '{TypeName}'. — Code generation is aborted because having the same type in constructor parameters (parent + Observable unique) causes DI resolution conflicts.

Auto-detects record types implementing ICommandRequest<TSuccess> or IQueryRequest<TSuccess> and generates IUsecaseCtxEnricher implementations. Works through interface implementation alone without a separate trigger attribute.

Auto-detection conditions:

  1. Must be a record declaration.
  2. Must implement the ICommandRequest<TSuccess> or IQueryRequest<TSuccess> interface.
  3. [CtxIgnore] must not be applied at the class level.
  4. The type must have public or internal accessibility (private/protected triggers FUNCTORIUM003 warning).

Exclusion attribute:

Functorium.Applications.Usecases
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Parameter,
AllowMultiple = false, Inherited = false)]
public sealed class CtxIgnoreAttribute : Attribute;

Promotion attribute:

Functorium.Abstractions.Observabilities
[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Property | AttributeTargets.Parameter,
AllowMultiple = false, Inherited = false)]
public sealed class CtxRootAttribute : Attribute;
Generated TypeDescription
{ContainingTypes}{RequestTypeName}CtxEnricherpartial class, IUsecaseCtxEnricher<TRequest, FinResponse<TSuccess>> implementation
public partial class {ContainingTypes}{RequestTypeName}CtxEnricher
: IUsecaseCtxEnricher<{RequestFullType}, FinResponse<{ResponseFullType}>>
{
// Push Request properties to LogContext
public IDisposable? EnrichRequestLog({RequestFullType} request);
// Push Response properties to LogContext (Succ Pattern matching)
public IDisposable? EnrichResponseLog(
{RequestFullType} request,
FinResponse<{ResponseFullType}> response);
// Extension points (user can implement as partial)
partial void OnEnrichRequestLog(
{RequestFullType} request,
List<IDisposable> disposables);
partial void OnEnrichResponseLog(
{RequestFullType} request,
FinResponse<{ResponseFullType}> response,
List<IDisposable> disposables);
// Helper methods
private static void PushRequestCtx(List<IDisposable> disposables, string fieldName, object? value);
private static void PushResponseCtx(List<IDisposable> disposables, string fieldName, object? value);
private static void PushRootCtx(...); // Generated only when [CtxRoot] attribute exists
}

ctx field naming rules:

Conditionctx Field PatternExample
Defaultctx.{containing_types}.request.{snake_case_name}ctx.place_order_command.request.customer_id
[CtxRoot] appliedctx.{snake_case_name}ctx.customer_id
Inherited from interfacectx.{interface_name}.{snake_case_name}ctx.operator_context.operator_id
Collection type...{snake_case_name}_countctx.place_order_command.request.items_count

Property filtering rules:

  • Scalar types (primitive, string, DateTime, Guid, enum, Option<T>, etc.): output value as-is
  • Collection type (List, Array, Seq, etc.): output count only with _count suffix
  • Complex types (class, record, struct): excluded
  • [CtxIgnore] applied properties: excluded
CodeSeverityMessage
FUNCTORIUM002Warningctx field '{FieldName}' has conflicting types: '{Type1}' ({Group1}) in '{Enricher1}' vs '{Type2}' ({Group2}) in '{Enricher2}'. — Dynamic mapping conflicts occur when different Enrichers assign different OpenSearch type groups to the same ctx field name.
FUNCTORIUM003Warning'{RequestType}' implements ICommandRequest/IQueryRequest but CtxEnricher cannot be generated because '{TypeName}' is {accessibility}. — Enrichers cannot be generated for private or protected types. Apply [CtxIgnore] to suppress the warning.

Auto-detects classes implementing IDomainEventHandler<TEvent> and generates IDomainEventCtxEnricher implementations for TEvent. Even if multiple Handlers exist for the same event type, the Enricher is generated only once.

Auto-detection conditions:

  1. Must be a class declaration.
  2. Must implement the IDomainEventHandler<TEvent> interface.
  3. TEvent must not be abstract.
  4. [CtxIgnore] must not be applied at the class level on TEvent.
  5. TEvent must have public or internal accessibility (private/protected triggers FUNCTORIUM004 warning).
Generated TypeDescription
{ContainingTypes}{EventTypeName}CtxEnricherpartial class, IDomainEventCtxEnricher<TEvent> implementation
public partial class {ContainingTypes}{EventTypeName}CtxEnricher
: IDomainEventCtxEnricher<{EventFullType}>
{
// Push event properties to LogContext
public IDisposable? EnrichLog({EventFullType} domainEvent);
// Extension points (user can implement as partial)
partial void OnEnrichLog(
{EventFullType} domainEvent,
List<IDisposable> disposables);
// Helper methods
private static void PushEventCtx(List<IDisposable> disposables, string fieldName, object? value);
private static void PushRootCtx(...); // Generated only when [CtxRoot] attribute exists
}

Property filtering rules:

Follows the same rules as CtxEnricherGenerator, with the addition that IDomainEvent default properties (OccurredAt, EventId, CorrelationId, CausationId) are automatically excluded. Properties implementing IValueObject or IEntityId<T> are converted to keyword via .ToString() call.

ctx field naming rules:

Conditionctx Field PatternExample
Top-level eventctx.{snake_case_event}.{snake_case_name}ctx.order_placed_event.order_id
Nested eventctx.{containing}.{snake_case_event}.{snake_case_name}ctx.order.created_event.amount
[CtxRoot] appliedctx.{snake_case_name}ctx.order_id
CodeSeverityMessage
FUNCTORIUM002Warningctx field type conflict (shares same ID with CtxEnricherGenerator)
FUNCTORIUM004Warning'{EventType}' implements IDomainEvent but DomainEventCtxEnricher cannot be generated because '{TypeName}' is {accessibility}. — Enrichers cannot be generated for private or protected event types. Apply [CtxIgnore] to suppress the warning.

Applying [UnionType] to an abstract partial record analyzes the internal sealed record cases and auto-generates Match/Switch pattern matching methods.

Functorium.Domains.ValueObjects.Unions
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class UnionTypeAttribute : Attribute;

Target: abstract partial record declarations with [UnionType] applied, requiring at least one directly inheriting sealed record case.

Generates the following members as partial extensions for the {TypeName} record.

Generated MemberSignature
Match<TResult>Accepts Func parameters for all cases and returns TResult
SwitchAccepts Action parameters for all cases and executes them
Is{CaseName}bool property — this is {CaseName}
As{CaseName}(){CaseName}? return — this as {CaseName}
public abstract partial record {TypeName}
{
// Pattern matching (with return value)
public TResult Match<TResult>(
Func<Case1, TResult> case1,
Func<Case2, TResult> case2,
...);
// Pattern matching (no return value)
public void Switch(
Action<Case1> case1,
Action<Case2> case2,
...);
// Type check properties
public bool IsCase1 => this is Case1;
public bool IsCase2 => this is Case2;
// Safe type conversion
public Case1? AsCase1() => this as Case1;
public Case2? AsCase2() => this as Case2;
}

Unreachable case handling: The default branch of Match/Switch throws UnreachableCaseException.

public sealed class UnreachableCaseException(object value)
: InvalidOperationException($"Unreachable case: {value.GetType().FullName}");

UnionTypeGenerator does not currently emit dedicated diagnostic codes. If there are no internal sealed record cases, code generation is skipped.