Skip to content

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:

  1. Reflection (Type.GetProperties()): Easy to write generic code, but slow, allocates memory, and breaks Native AOT (due to trimming).
  2. 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.

FeatureStandard ReflectionSannr Shadow Types
Discoverytype.GetProperties() (Slow, Runtime)UserShadow.Metadata (Instant, Compile-Time)
Accessprop.GetValue(obj) (Boxing, Slow)UserShadow.GetEmail(obj) (Direct Access, Fast)
AOT SupportRequires configurationNative by default
MemoryAllocates arrays & objectsZero Allocation
CloningSerialization (Slow)DeepClone() (Hand-optimized code)

Usage

1. Mark Your Class

Add [SannrReflect] to your class. Optionally, mark sensitive properties with [Pii].

csharp
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.

csharp
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.

csharp
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.

csharp
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.

csharp
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:

csharp
// 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:

  1. Trimming Safe: The compiler sees direct method calls.
  2. Fast: It can be inlined by the JIT.
  3. Simple: No magic, just code you would have written yourself.

Released under the MIT License.