Static Reflection with Shadow Types
Sannr introduces Static Reflection, a breakthrough feature that solves the conflict between Native AOT performance requirements and developer convenience.
Instead of unreliable and slow runtime reflection, Sannr generates "Shadow Types" at compile-timestatic companion classes that provide high-performance accessors, metadata, and deep-cloning logic with zero runtime overhead.
The Problem
In modern .NET development, you often face a tradeoff:
- Reflection (
Type.GetProperties()): Easy to write generic code, but slow, allocates memory, and breaks Native AOT (due to trimming). - Manual Code: Fast and safe, but tedious to write and maintain (e.g., writing manual mappers or clones).
The Solution: Shadow Types
Sannr generates a static *Shadow class for every class you mark with [SannrReflect]. This shadow class contains hardcoded, optimized logic to interact with your data.
| Feature | Standard Reflection | Sannr Shadow Types |
|---|---|---|
| Discovery | type.GetProperties() (Slow, Runtime) | UserShadow.Metadata (Instant, Compile-Time) |
| Access | prop.GetValue(obj) (Boxing, Slow) | UserShadow.GetEmail(obj) (Direct Access, Fast) |
| AOT Support | Requires configuration | Native by default |
| Memory | Allocates arrays & objects | Zero Allocation |
| Cloning | Serialization (Slow) | DeepClone() (Hand-optimized code) |
Usage
1. Mark Your Class
Add [SannrReflect] to your class. Optionally, mark sensitive properties with [Pii].
using Sannr;
[SannrReflect]
public class User
{
public int Id { get; set; }
[Pii] // Marks this property as sensitive
public string Email { get; set; }
public List<Address> Addresses { get; set; }
}
[SannrReflect]
public class Address
{
public string City { get; set; }
}2. Use the Shadow Type
Sannr generates a UserShadow class in the same namespace.
Fast Property Access
Read and write properties without reflection overhead or boxing.
var user = new User { Id = 1, Email = "test@example.com" };
// Fast Read
int id = UserShadow.GetId(user); // No boxing!
// Fast Write
UserShadow.SetEmail(user, "new@example.com");Metadata & PII Checking
Check for attributes without GetCustomAttribute.
if (UserShadow.IsEmailPii)
{
Console.WriteLine("Email is PII - Masking...");
}
// Compile-time constants
Console.WriteLine($"Type: {UserShadow.Metadata.TypeName}");
Console.WriteLine($"Properties: {UserShadow.Metadata.PropertyCount}");High-Performance Deep Cloning
Deep cloning usually requires slow serialization (JSON) or complex reflection. Sannr generates efficient, recursive cloning logic.
var original = new User
{
Id = 1,
Addresses = new List<Address> { new Address { City = "Oslo" } }
};
// Creates a full deep copy, including the list and its items
var clone = UserShadow.DeepClone(original);
clone.Addresses[0].City = "Bergen";
// original.Addresses[0].City is still "Oslo"Generic Visitor (zero-allocation)
Iterate over properties efficiently.
UserShadow.Visit(user, (name, value, isPii) =>
{
if (isPii)
Logger.Log($"{name}: ***");
else
Logger.Log($"{name}: {value}");
});How It Works
The Source Generator inspects your code and writes a C# file that looks like this:
// Generated by Sannr
public static class UserShadow
{
public static int GetId(User instance) => instance.Id;
public static void SetId(User instance, int value) => instance.Id = value;
public static bool IsEmailPii => true;
public static User DeepClone(User source)
{
if (source == null) return null;
var clone = new User
{
Id = source.Id,
Email = source.Email
};
if (source.Addresses != null)
{
clone.Addresses = new List<Address>(source.Addresses.Count);
foreach (var item in source.Addresses)
{
clone.Addresses.Add(AddressShadow.DeepClone(item));
}
}
return clone;
}
}This code is:
- Trimming Safe: The compiler sees direct method calls.
- Fast: It can be inlined by the JIT.
- Simple: No magic, just code you would have written yourself.