Configuration
The Intellenum configuration is a powerful way to customize code generation. There are two levels of configuration.
Global Configuration
The global configuration is an assembly-level attribute. The available options are:
underlyingType
- the type of the primitive that is being wrapped, e.g.int
orlong
- defaults toint
conversions
- specifies what conversion code is generated. See the Integrations section in the wiki for more details - defaults toConversions.Default
which generates type converters and a converter to handle serialization usingSystem.Text.Json
throws
- specifies the type of exception thrown when validation fails. The exception type must derive fromSystem.Exception
and have one public constructor that takes anint
—defaults toValueObjectValidationException
customizations
- simple customization switches.debuggerAttributes
- specifies the level that debug attributes are written as some IDEs don’t support all of them, e.g., Rider - defaults toDebuggerAttributeGeneration.Full
which generatesDebuggerDisplay
and a debugger proxy type for IDEs that support themcomparison
- specifies which comparison code is generated - defaults toComparisonGeneration.UseUnderlying
which hoists anyIComparable
implementations from the primitiveopenApiSchemaCustomizations
- determines what is generated to assist in OpenAPI scenarios, for instance, generate a schema filter for Swashbuckle, or generate an extension method withMapType
calls - defaults toOpenApiSchemaCustomizations.Omit
explicitlySpecifyTypeInValueObject
- specifies whether individual value objects should explicitly define the primitive type that they wrap - defaults tofalse
tryFromGeneration
- specifies what to write forTryFrom
methods—defaults toTryFromGeneration.GenerateBoolAndErrorOrMethods
isInitializedMethodGeneration
- specifies whether to generate anIsInitialized()
method - defaults toIsInitializedMethodGeneration.Generate
systemTextJsonConverterFactoryGeneration
- determines whether to write a factory for STJ converters which are useful as a replacement to Reflect in AOT/trimmed scenarios—defaults toSystemTextJsonConverterFactoryGeneration.Generate
staticAbstractsGeneration
- determines whether to write static abstract code - defaults toStaticAbstractsGeneration.Omit
Here’s an example of global configuration:
[assembly: IntellenumDefaults(
underlyingType: typeof(int),
conversions: Conversions.Default,
throws: typeof(ValueObjectValidationException))]
Per-Value Object Configuration
Each value object can have its own configuration that overrides the global configuration.
[ValueObject(underlyingType: typeof(long))]
public partial struct MyValueObject : IValueObject
{
// ...
}
This configuration uses the default values for conversions
and throws
from the global configuration, but overrides the default underlyingType
to use long
instead of int
.
Validation
The configuration is validated at compile time to ensure it’s valid. Several code analysis warnings exist for invalid configuration, including:
- When you specify an exception that does not derive from
System.Exception
- When your exception does not have one public constructor that takes an
int
- When the combination of conversions does not match an entry
The following code demonstrates the use of the IntellenumDefaultsAttribute
by referencing: src/Intellenum/ManageAttributes.cs
{
public static IntellenumConfigurationBuildResult GetDefaultConfigFromGlobalAttribute(Compilation compilation)
{
ImmutableArray assemblyAttributes = compilation.Assembly.GetAttributes();
if (assemblyAttributes.IsDefaultOrEmpty)
{
return IntellenumConfigurationBuildResult.Null;
}
INamedTypeSymbol? allThatMatchByName = compilation.GetTypeByMetadataName("Intellenum.IntellenumDefaultsAttribute");
if (allThatMatchByName is null)
{
return IntellenumConfigurationBuildResult.Null;
}
AttributeData? matchingAttribute = assemblyAttributes.SingleOrDefault(aa =>
allThatMatchByName.Equals(aa.AttributeClass, SymbolEqualityComparer.Default));
if (matchingAttribute is null)
{
return IntellenumConfigurationBuildResult.Null;
}
IntellenumConfigurationBuildResult globalConfig = TryBuildConfigurationFromAttribute(matchingAttribute);
return globalConfig;
}
///
/// Gets global default configuration from any global (assembly) attribute.
/// If none are specified, then the default configuration is used.
/// If some are specified, then they are validated.
/// If anything is invalid, a compilation error is raised.
///
///
///
///
public static IntellenumConfigurationBuildResult GetDefaultConfigFromGlobalAttribute(
ImmutableArray defaults,
Compilation compilation)
{
if (defaults.IsDefaultOrEmpty)
{
// No global defaults
return IntellenumConfigurationBuildResult.Null;
}
var assemblyAttributes = compilation.Assembly.GetAttributes();
if (assemblyAttributes.IsDefaultOrEmpty)
{
return IntellenumConfigurationBuildResult.Null;
}
INamedTypeSymbol? allThatMatchByName = compilation.GetTypeByMetadataName("Intellenum.IntellenumDefaultsAttribute");
if (allThatMatchByName is null)
{
return IntellenumConfigurationBuildResult.Null;
}
AttributeData? matchingAttribute = assemblyAttributes.SingleOrDefault(aa =>
allThatMatchByName.Equals(aa.AttributeClass, SymbolEqualityComparer.Default));
if (matchingAttribute == null)
{
return IntellenumConfigurationBuildResult.Null;
}
IntellenumConfigurationBuildResult globalConfig = TryBuildConfigurationFromAttribute(matchingAttribute);
return globalConfig;
}
public static IntellenumConfigurationBuildResult TryBuildConfigurationFromAttribute(AttributeData matchingAttribute)
{
IntellenumConfigurationBuildResult buildResult = new IntellenumConfigurationBuildResult();
INamedTypeSymbol? underlyingType = null;
Conversions conversions = Conversions.Default;
Customizations customizations = Customizations.None;
DebuggerAttributeGeneration debuggerAttributes = DebuggerAttributeGeneration.Default;
bool hasErroredAttributes = false;
INamedTypeSymbol attrClass = matchingAttribute.AttributeClass ?? throw new InvalidOperationException("Expected an attribute class");
var isBaseGenericType = attrClass.BaseType!.IsGenericType;
if (!matchingAttribute.ConstructorArguments.IsEmpty || isBaseGenericType)
{
// make sure we don't have any errors
ImmutableArray args = matchingAttribute.ConstructorArguments;
foreach (TypedConstant arg in args)
{
if (arg.Kind == TypedConstantKind.Error)
{
hasErroredAttributes = true;
}
}
// find which constructor to use, it could be the generic attribute (> C# 11), or the non-generic.
if (attrClass.IsGenericType || isBaseGenericType)
{
PopulateFromGenericAttribute(matchingAttribute, args);
}
else
{
PopulateFromNonGenericAttribute(args);
}
}
if (!matchingAttribute.NamedArguments.IsEmpty)
{
foreach (KeyValuePair arg in matchingAttribute.NamedArguments)
{
TypedConstant typedConstant = arg.Value;
if (typedConstant.Kind == TypedConstantKind.Error)
{
hasErroredAttributes = true;
}
else
{
switch (arg.Key)
{
case "underlyingType":
underlyingType = (INamedTypeSymbol?) typedConstant.Value!;
break;
case "invalidExceptionType":
break;
case "conversions":
conversions = (Conversions) (typedConstant.Value ?? Conversions.Default);
break;
case "customizations":
customizations = (Customizations) (typedConstant.Value ?? Customizations.None);
break;
case "debuggerAttributes":
debuggerAttributes = (DebuggerAttributeGeneration) (typedConstant.Value ?? DebuggerAttributeGeneration.Full);
break;
}
}
}
}
if (hasErroredAttributes)
{
// skip further generator execution and let compiler generate the errors
return IntellenumConfigurationBuildResult.Null;
}
if (!conversions.IsValidFlags())
{
var syntax = matchingAttribute.ApplicationSyntaxReference?.GetSyntax();
if (syntax is not null)
{
buildResult.AddDiagnostic(DiagnosticsCatalogue.InvalidConversions(syntax.GetLocation()));
}
}
if (!customizations.IsValidFlags())
{
var syntax = matchingAttribute.ApplicationSyntaxReference?.GetSyntax();
if (syntax is not null)
{
buildResult.AddDiagnostic(DiagnosticsCatalogue.InvalidCustomizations(syntax.GetLocation()));
}
}
buildResult.ResultingConfiguration = new IntellenumConfiguration(
underlyingType,
conversions,
customizations,
debuggerAttributes);
return buildResult;
void PopulateFromGenericAttribute(
AttributeData attributeData,
ImmutableArray args)
{
var isDerivedFromGenericAttribute =
attributeData.AttributeClass!.BaseType!.FullName()!.StartsWith("Intellenum.IntellenumAttribute
var type = isDerivedFromGenericAttribute && attributeData.AttributeClass!.TypeArguments.IsEmpty
? attributeData.AttributeClass!.BaseType!.TypeArguments[0] as INamedTypeSymbol
: attributeData.AttributeClass!.TypeArguments[0] as INamedTypeSymbol;
switch (args.Length)
{
case 3:
if (args[2].Value != null)
{
debuggerAttributes = (DebuggerAttributeGeneration) args[2].Value!;
}
goto case 2;
case 2:
if (args[1].Value != null)
{
customizations = (Customizations) args[1].Value!;
}
goto case 1;
case 1:
if (args[0].Value != null)
{
conversions = (Conversions) args[0].Value!;
}
break;
}
underlyingType = type;
}
void PopulateFromNonGenericAttribute(ImmutableArray args)
{
switch (args.Length)
{
case 4:
if (args[3].Value != null)
{
debuggerAttributes = (DebuggerAttributeGeneration) args[3].Value!;
}
goto case 3;
case 3:
if (args[2].Value != null)
{
customizations = (Customizations) args[2].Value!;
}
goto case 2;
case 2:
if (args[1].Value != null)
{
conversions = (Conversions) args[1].Value!;
}
goto case 1;
case 1:
underlyingType = (INamedTypeSymbol?) args[0].Value;
break;
}
}
}
///
/// Tries to get the syntax element for any matching attribute that might exist in the provided context.
///
///
/// The syntax of the attribute if it matches the global defaults attribute, otherwise null.
public static AttributeSyntax? TryGetAssemblyLevelDefaultsAttribute(GeneratorSyntaxContext context)
{
// we know the node is a AttributeListSyntax thanks to IsSyntaxTargetForGeneration
var attributeListSyntax = (AttributeListSyntax) context.Node;
foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes)
{
if (context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol is not IMethodSymbol attributeSymbol)
{
continue;
}
INamedTypeSymbol attributeContainingTypeSymbol = attributeSymbol.ContainingType;
string fullName = attributeContainingTypeSymbol.ToDisplayString();
if (fullName == "Intellenum.IntellenumDefaultsAttribute")
{
return attributeSyntax;
}
}
return null;
}
}
The TryGetAssemblyLevelDefaultsAttribute
function attempts to get the syntax element for any matching attribute that may exist in the provided context. src/Intellenum/ManageAttributes.cs
///
/// Tries to get the syntax element for any matching attribute that might exist in the provided context.
///
///
/// The syntax of the attribute if it matches the global defaults attribute, otherwise null.
public static AttributeSyntax? TryGetAssemblyLevelDefaultsAttribute(GeneratorSyntaxContext context)
{
// we know the node is a AttributeListSyntax thanks to IsSyntaxTargetForGeneration
var attributeListSyntax = (AttributeListSyntax) context.Node;
foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes)
{
if (context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol is not IMethodSymbol attributeSymbol)
{
continue;
}
INamedTypeSymbol attributeContainingTypeSymbol = attributeSymbol.ContainingType;
string fullName = attributeContainingTypeSymbol.ToDisplayString();
if (fullName == "Intellenum.IntellenumDefaultsAttribute")
{
return attributeSyntax;
}
}
return null;
}
}
The configuration is used to generate the code for each value object, using the ValueObject
and VogenDefaults
attributes.
Code Snippets
The following code snippets demonstrate how the configuration is used in the codebase.
src/Benchmarks/ToStringBenchmarks.cs
: src/Benchmarks/ToStringBenchmarks.cs
Code: [GlobalSetup]
public void Setup()
{
}
src/Benchmarks/FromValueBenchmarks.cs
: src/Benchmarks/FromValueBenchmarks.cs
Code: [GlobalSetup]
public void Setup()
{
}
Top-Level Directory Explanations
samples/ - This directory contains example projects demonstrating the usage of Intellenum.
samples/Intellenum.Examples/ - Contains various example projects demonstrating different aspects of Intellenum, such as serialization, conversion, syntax examples, types, typical scenarios, and more.
samples/WebApplication/ - Contains a sample web application that uses Intellenum.
src/ - This directory contains the source code of the Intellenum library.
src/Benchmarks/ - Contains benchmark tests for the Intellenum library.
src/Intellenum.CodeFixers/ - Contains code fixers for the Intellenum library.
src/Intellenum.SharedTypes/ - Contains shared types used across the Intellenum library.
src/Intellenum/ - Contains the main source code for the Intellenum library. This directory is further divided into subdirectories for diagnostics, extensions, generators, member building, properties, rules, static constructor building, templates, and more.
tests/ - This directory contains test projects for the Intellenum library.
tests/AnalyzerTests/ - Contains unit tests for the Intellenum analyzer.
tests/ConsumerTests/ - Contains tests for consuming the Intellenum library.
tests/Intellenum.Tests/ - Contains additional tests for the Intellenum library.
tests/Shared/ - Contains shared test files.
tests/SnapshotTests/ - Contains snapshot tests for the Intellenum library.