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.
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
GeneratedMetadataAttribute. Every custom metadata attribute that should be discovered by the generator derives from this type.
GeneratedPropertyMetadata<TAttribute>, GeneratedTypeMetadata, and the generated invocation delegate type.
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>
- Publish the same packages to a private NuGet feed when multiple projects need to reuse them.
- Packages built from this repository are intended to be freely reusable by any consuming project.
- Bump the package version and update consuming projects whenever a new reusable build is published.
- Verify the generator package exposes
Long.Metadata.Generator.dllas an analyzer in the final.nupkg. - The consumer app should reference the runtime package normally and should not load the generator at runtime.
- Use
GetAllTypes()for generated type inventory andGetProperties<TAttribute>()for attributed property metadata.
6. How The Generator Works
The generator selects attributed PropertyDeclarationSyntax nodes and all class/interface declarations.
It reads real IPropertySymbol and INamedTypeSymbol values instead of guessing from syntax strings.
It captures property attributes, type attributes, type accessibility, abstract/sealed/interface flags, base types, and interfaces.
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)));
GetAllTypes()returns the full generated type inventory for the current compilation.GetTypes<TAttribute>()still returns the attributed type view when attributes are useful.- Each item includes the generated
Type, display names, access modifier, abstract/sealed/interface flags, base types, and interfaces. - The runtime path still does not scan assemblies or use reflection attribute lookup.
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(...);
}
}
- No reflection invoke is used.
voidmethods returnnull.- A
nullproperty value returnsnull. - A method that was not generated throws
MissingMethodException.
9. Result Achieved
- Supports .NET 8 and .NET 10 for runtime and abstractions.
- Runs as an incremental source generator at compile time.
- Uses the Roslyn semantic model to detect attributes derived from
GeneratedMetadataAttribute. - Generates strongly typed registries by attribute type.
- Avoids runtime reflection scanning, making the runtime path more Native AOT-friendly.
- Supports generated class/interface metadata for DI or business registries.
- Supports method invocation on decorated class properties through generated direct calls.
- Includes a sample consumer and unit tests that validate generated output.
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