Skip to content

Attribute Data Models

Preview SourceGenerator Framework Reviewed 2026-09-16 purview-dev/sourcegenerator-framework analyzers code-generation compile-time compiler csharp developer-experience developer-tools devex dotnet incremental-generator metaprogramming nuget roslyn source-generator source-generators testing

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.

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.
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);
}
}

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.

[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
);

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.

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.

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.

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
);

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.

This documentation is part of the MIT-licensed Purview.SourceGeneratorFramework project.