Long.Metadata

Compile-time metadata registries for .NET Native AOT

This guide explains how to implement the library, how the source generator works, and why the runtime path does not need reflection scanning. The project is intended to be free for everyone to use and reuse.

1. Goal

Long.Metadata solves the problem of reading metadata from attributed properties and source-declared application types without scanning reflection metadata at runtime. This matters for Native AOT applications because reflection metadata can be trimmed and often requires explicit preservation.

Desired outcome: developers call GeneratedMetadata.GetProperties<IgnorePropertyAttribute>() or GeneratedMetadata.GetAllTypes() and receive metadata that was discovered and generated during compilation.

2. Free For Everyone

Long.Metadata is intended to be free for everyone to use, copy, learn from, adapt, package, and reuse in personal, internal, or commercial projects.

If this repository or packages built from it are published publicly, include a LICENSE file so downstream users have explicit legal terms.

3. Architecture

Long.Metadata.Abstractions Contains GeneratedMetadataAttribute. Every custom metadata attribute that should be discovered by the generator derives from this type.
Long.Metadata.Runtime Contains runtime models such as GeneratedPropertyMetadata<TAttribute>, GeneratedTypeMetadata, and the generated invocation delegate type.
Long.Metadata.Generator An incremental source generator that uses the Roslyn semantic model to find valid attributed properties and build a source-declared class/interface inventory.

4. Implementing It In A Consumer Project

Reference the runtime and generator

<ItemGroup>
  <ProjectReference Include="..\Long.Metadata.Runtime\Long.Metadata.Runtime.csproj" />
  <ProjectReference Include="..\Long.Metadata.Generator\Long.Metadata.Generator.csproj"
                    OutputItemType="Analyzer"
                    ReferenceOutputAssembly="false" />
</ItemGroup>

Create a custom attribute

using Long.Metadata;

public sealed class IgnorePropertyAttribute : GeneratedMetadataAttribute
{
}

Decorate a property

public sealed class User
{
    [IgnoreProperty]
    public string Password { get; set; } = "";
}

Read metadata without reflection

var ignored = GeneratedMetadata.GetProperties<IgnorePropertyAttribute>();

foreach (var property in ignored)
{
    Console.WriteLine($"{property.DeclaringTypeDisplayName}.{property.PropertyName}");
}

5. Adopt It In Another Project

Consumers need the runtime assembly as a normal reference and the generator assembly as an analyzer. The runtime types are used by application code, while the generator only runs during compilation. The code and packages are intended to be free for everyone to reuse.

Option 1: reference the source projects

Copy or vendor the src/Long.Metadata.* projects into a shared folder, submodule, or nearby repository, then reference the runtime normally and the generator as an analyzer.

<ItemGroup>
  <ProjectReference Include="..\Long.Metadata\src\Long.Metadata.Runtime\Long.Metadata.Runtime.csproj" />
  <ProjectReference Include="..\Long.Metadata\src\Long.Metadata.Generator\Long.Metadata.Generator.csproj"
                    OutputItemType="Analyzer"
                    ReferenceOutputAssembly="false" />
</ItemGroup>

Option 2: pack NuGet packages

dotnet pack src/Long.Metadata.Abstractions/Long.Metadata.Abstractions.csproj -c Release -o artifacts/packages
dotnet pack src/Long.Metadata.Runtime/Long.Metadata.Runtime.csproj -c Release -o artifacts/packages
dotnet pack src/Long.Metadata.Generator/Long.Metadata.Generator.csproj -c Release -o artifacts/packages

Use a local package feed

dotnet nuget add source ./artifacts/packages --name LongMetadataLocal

Reference the packages

dotnet add package Long.Metadata.Runtime --version 0.1.0 --source ./artifacts/packages
dotnet add package Long.Metadata.Generator --version 0.1.0 --source ./artifacts/packages

Keep the generator package configured as an analyzer in the final project file.

<ItemGroup>
  <PackageReference Include="Long.Metadata.Runtime" Version="0.1.0" />
  <PackageReference Include="Long.Metadata.Generator"
                    Version="0.1.0"
                    PrivateAssets="all"
                    OutputItemType="Analyzer"
                    ReferenceOutputAssembly="false" />
</ItemGroup>

6. How The Generator Works

1 Syntax filter

The generator selects attributed PropertyDeclarationSyntax nodes and all class/interface declarations.

2 Semantic model

It reads real IPropertySymbol and INamedTypeSymbol values instead of guessing from syntax strings.

3 Metadata capture

It captures property attributes, type attributes, type accessibility, abstract/sealed/interface flags, base types, and interfaces.

4 Emit registry

It generates static arrays, generic lookup APIs, all-property/type APIs, and optional direct method invokers.

Generated code concept

public static class GeneratedMetadata
{
    public static IReadOnlyList<GeneratedPropertyMetadata<TAttribute>> GetProperties<TAttribute>()
        where TAttribute : GeneratedMetadataAttribute
    {
        if (typeof(TAttribute) == typeof(IgnorePropertyAttribute))
        {
            return (IReadOnlyList<GeneratedPropertyMetadata<TAttribute>>)(object)__IgnorePropertyAttribute;
        }

        return Array.Empty<GeneratedPropertyMetadata<TAttribute>>();
    }
}

Important point: runtime code reads generated static arrays. It does not scan assemblies, scan types, scan properties, or call GetCustomAttributes().

7. Type Metadata For DI Or Business Registries

The generator tracks all source-declared classes and interfaces at compile time. Each generated type entry includes its access modifier, whether it is abstract, sealed, or an interface, plus its base-type chain and implemented interfaces.

public interface IUserService
{
}

public sealed class UserService : IUserService
{
}

public abstract class BaseHandler
{
}

public class ProductHandler : BaseHandler
{
}

var services = GeneratedMetadata.GetAllTypes()
    .Where(type => type.Accessibility == "public" &&
        !type.IsAbstract &&
        !type.IsInterface &&
        type.IsAssignableTo(typeof(IUserService)));

8. Invoking Methods On Class Properties

If the decorated property is a reference type, the generator creates invokers for public instance methods that are parameterless and non-generic. The runtime API is metadata.Invoke(ownerInstance, methodName).

public sealed class AuditedPropertyAttribute : GeneratedMetadataAttribute
{
}

public sealed class User
{
    [AuditedProperty]
    public Account Account { get; set; } = new("active");
}

public sealed class Account(string status)
{
    public string GetStatus() => status;
}

var metadata = GeneratedMetadata.GetProperties<AuditedPropertyAttribute>().Single();
var result = metadata.Invoke(user, "GetStatus");

Generated invoker concept

private static object? __User_Account_Invoker(object declaringInstance, string methodName)
{
    var propertyValue = ((User)declaringInstance).Account;
    if (propertyValue is null)
    {
        return null;
    }

    switch (methodName)
    {
        case "GetStatus":
            return propertyValue.GetStatus();
        default:
            throw new MissingMethodException(...);
    }
}

9. Result Achieved

Run commands

dotnet build Long.Metadata.slnx
dotnet test Long.Metadata.slnx
dotnet run --project samples/Long.Metadata.Sample/Long.Metadata.Sample.csproj --framework net8.0

Sample output

User.Password: string
Account.GetStatus(): active
BaseHandler: public abstract
IUserService: public interface
public sealed class UserService implements IUserService
public class ProductHandler inherits BaseHandler