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 or long - defaults to int
  • conversions - specifies what conversion code is generated. See the Integrations section in the wiki for more details - defaults to Conversions.Default which generates type converters and a converter to handle serialization using System.Text.Json
  • throws - specifies the type of exception thrown when validation fails. The exception type must derive from System.Exception and have one public constructor that takes an int—defaults to ValueObjectValidationException
  • 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 to DebuggerAttributeGeneration.Full which generates DebuggerDisplay and a debugger proxy type for IDEs that support them
  • comparison - specifies which comparison code is generated - defaults to ComparisonGeneration.UseUnderlying which hoists any IComparable implementations from the primitive
  • openApiSchemaCustomizations - determines what is generated to assist in OpenAPI scenarios, for instance, generate a schema filter for Swashbuckle, or generate an extension method with MapType calls - defaults to OpenApiSchemaCustomizations.Omit
  • explicitlySpecifyTypeInValueObject - specifies whether individual value objects should explicitly define the primitive type that they wrap - defaults to false
  • tryFromGeneration - specifies what to write for TryFrom methods—defaults to TryFromGeneration.GenerateBoolAndErrorOrMethods
  • isInitializedMethodGeneration - specifies whether to generate an IsInitialized() method - defaults to IsInitializedMethodGeneration.Generate
  • systemTextJsonConverterFactoryGeneration - determines whether to write a factory for STJ converters which are useful as a replacement to Reflect in AOT/trimmed scenarios—defaults to SystemTextJsonConverterFactoryGeneration.Generate
  • staticAbstractsGeneration - determines whether to write static abstract code - defaults to StaticAbstractsGeneration.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.

Code: [GlobalSetup]
              public void Setup()
              {
              }
          
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.