Attribute Data Models
Attribute Data Models
Section titled “Attribute Data Models”AttributeDataModelGenerator generates readonly record struct parser models for .NET attributes.
Instead of hand-writing FromAttributeData methods for every attribute you inspect in a source
generator, declare a readonly partial record struct with [Generate] and the generator fills in the
Empty sentinel, FromAttributeData overloads, and property extraction logic.
The generator is implemented in Purview.SourceGeneratorFramework.Generators and ships inside the
Purview.SourceGeneratorFramework package under analyzers/dotnet/cs/, so it runs automatically
when you reference the package.
Marker attributes
Section titled “Marker attributes”The generator emits marker attributes into your compilation:
| Attribute | Purpose |
|---|---|
[Generate(Type targetAttribute)] |
Placed on a readonly partial record struct to opt into generation. |
[Generate(string targetAttribute)] |
Resolves the attribute by fully-qualified name. Use when the attribute type is not available in the generator’s compilation (e.g. LengthAttribute in .NET 8+ or a self-generated attribute). |
[Property] |
A record parameter is populated from a named attribute property (the property name is inferred from the parameter name unless overridden). |
[Property(string name)] |
Explicit named property source. |
[Property(..., DefaultValue = ...)] |
Fallback value when the named property is not present. |
[Argument] |
Populated from a constructor argument by parameter name. |
[Argument(int index)] |
Constructor argument by parameter index. |
[Argument(string name)] |
Constructor argument by parameter name. |
[Argument(..., DefaultValue = ...)] |
Fallback when the constructor argument is not present. |
[NestedModel] |
Populated by recursively calling FromAttributeData on a nested generated model. |
[Exclude] |
Skips auto-discovery for this parameter. |
[GenericTypeArgument] |
Populated from a generic type argument of the attribute class. |
[GenericTypeArgument(int index)] |
Generic type argument by position. |
[GenericTypeArgument(string name)] |
Generic type argument by type parameter name. |
Manual mapping
Section titled “Manual mapping”using Microsoft.CodeAnalysis;using Purview.SourceGeneratorFramework.Generators;using System.ComponentModel.DataAnnotations;
namespace MySourceGenerator.Models;
[Generate(typeof(RequiredAttribute))]public readonly partial record struct RequiredAttributeData( bool AllowEmptyStrings);Generated output:
readonly record struct RequiredAttributeData(bool Exists, bool AllowEmptyStrings){ public static readonly RequiredAttributeData Empty = new(false, default(bool));
public static RequiredAttributeData FromAttributeData(ImmutableArray<AttributeData> attributes) { // ... }
public static RequiredAttributeData FromAttributeData(AttributeData attributeData) { if (!TargetAttribute.Equals(attributeData.AttributeClass)) return Empty;
attributeData.TryGetNamedArgument<bool>("AllowEmptyStrings", out var allowEmptyStrings); return new(true, allowEmptyStrings); }}String target names
Section titled “String target names”When the attribute type is not referenced in the generator project, pass the fully-qualified name as
a string. This is useful for attributes newer than the generator’s target framework (e.g.
LengthAttribute in .NET 8+) or attributes that are generated by the same generator:
[Generate("System.ComponentModel.DataAnnotations.RequiredAttribute")]public readonly partial record struct RequiredAttributeData( bool AllowEmptyStrings);A plain type name ("RequiredAttribute") can also be used, which matches an attribute in the global
namespace or in any namespace. AutoDiscover requires the real Type overload because it must
inspect the attribute’s constructors and properties.
Constructor arguments
Section titled “Constructor arguments”[Generate(typeof(LengthAttribute))]public readonly partial record struct LengthAttributeData( [Argument(0)] int MinimumLength, [Argument(1)] int MaximumLength);Or by constructor parameter name:
[Generate(typeof(StringLengthAttribute))]public readonly partial record struct StringLengthAttributeData( [Argument("maximumLength", DefaultValue = 2147483647)] int MaximumLength, int MinimumLength);Nested models
Section titled “Nested models”Any property whose type is itself annotated with [Generate] can be populated as a nested model.
This is useful for shared base attribute data, such as ValidationAttribute in
System.ComponentModel.DataAnnotations:
[Generate(typeof(ValidationAttribute), MatchByInheritance = true)]public readonly partial record struct ValidationAttributeData( [Property] string? ErrorMessage, [Property] string? ErrorMessageResourceName, [Property] ITypeSymbol? ErrorMessageResourceType);
[Generate(typeof(RequiredAttribute))]public readonly partial record struct RequiredAttributeData( bool AllowEmptyStrings, [NestedModel] ValidationAttributeData ValidationAttribute);Because ValidationAttributeData uses MatchByInheritance = true, it matches any attribute that
derives from ValidationAttribute, including RequiredAttribute.
Generic type arguments
Section titled “Generic type arguments”If the attribute class is generic, a record parameter can be populated from the attribute’s type argument:
[Generate(typeof(MyGenericAttribute<>))]public readonly partial record struct MyGenericAttributeData<T>( [GenericTypeArgument] T Value);Use [GenericTypeArgument(0)] or [GenericTypeArgument("TValue")] to disambiguate when the
attribute has multiple type parameters.
Auto-discovery
Section titled “Auto-discovery”For simple attributes you can let the generator discover all constructor parameters and public named properties automatically:
[Generate(typeof(RequiredAttribute), AutoDiscover = true)]public readonly partial record struct RequiredAttributeData;This generates the same RequiredAttributeData as the manual example above. Nested models are not
auto-discovered; declare them explicitly if needed.
Default values
Section titled “Default values”DefaultValue provides a runtime fallback when the attribute does not contain the requested property
or argument. The Empty sentinel always uses default(T) for every property (including an Exists
field set to false):
[Generate(typeof(HostKitAttribute))]public readonly partial record struct HostKitAttributeData( [Argument("name", DefaultValue = "MyApp")] string Name, [Argument("generateOptions", DefaultValue = true)] bool GenerateOptions);Type library integration
Section titled “Type library integration”A [TypeRef] member declared with GenerateFullNameConst produces a public const string {Member}FullName, which can be used as the [Generate] target of an attribute-data model instead of
a typeof(...) value — see Type-Library.md
for the full example.
Because the TypeLibrary class is emitted through TypeLibraryGenerator’s main pipeline, its
constants are not present in the compilation that AttributeDataModelGenerator’s
ForAttributeWithMetadataName pipeline sees (only post-initialization output is shared between
generators in a single pass). AttributeDataModelGenerator therefore reassembles the target from the
argument’s member-access expression — guarded so the root identifier must match a
[GenerateTypeLibrary] spec’s ClassName — and resolves it against the compilation.
For [Argument]/[Property] members marked IsEnum = true, a DefaultValue supplied as a bare
member name (for example "Inherit") is expanded to the fully-qualified
"{EnumFullName}.{Member}" form using the enum type of the target attribute’s matching constructor
parameter (for [Argument]) or property (for [Property]). Fully-qualified defaults and defaults
whose enum type cannot be resolved are emitted unchanged.
License
Section titled “License”This documentation is part of the MIT-licensed Purview.SourceGeneratorFramework project.