Skip to content

Pouria7/AnAspect.OneOf

Repository files navigation

AnAspect.OneOf - IL-Weaved Discriminated Unions

A high-performance discriminated union library for C# that uses IL weaving (Fody) to eliminate memory overhead while maintaining full type safety at compile time.

🚀 Key Innovation

Traditional OneOf implementations use structs or classes with discriminators, causing significant memory overhead. This library provides the same developer experience but rewrites the IL at build time to use plain object references, achieving:

  • 30-57% memory reduction compared to traditional OneOf implementations
  • Zero runtime overhead for type checking (direct is operator)
  • Full IntelliSense support at development time
  • Type safety maintained at compile time

📊 Benchmark Results (.NET 10)

Test 1: Traditional OneOf<User, Admin> (reference types)
  Allocated: 10,413,984 bytes (104.00 bytes per iteration)
  Per OneOf wrapper: ~40.00 bytes

Test 2: IL-Weaved OneOf<User, Admin> (object reference only)
  Allocated: 7,199,992 bytes (71.00 bytes per iteration)
  Per OneOf wrapper: ~7.00 bytes

Memory Savings: 30.9% reduction

Test 3: Traditional OneOf<Point, int> (value types)
  Allocated: 5,601,560 bytes (56.00 bytes per iteration)

Test 4: IL-Weaved OneOf<Point, int> (value types)
  Allocated: 2,402,840 bytes (24.00 bytes per iteration)

Memory Savings: 57.1% reduction

🎯 How It Works

At Compile Time (What You Write):

OneOf<int, string> value = OneOf<int, string>.From(42);
if (value.IsT1)
{
    int result = value.AsT1();
    Console.WriteLine(result);
}

After IL Weaving (What Gets Executed):

object value = (object)42;  // Direct boxing
if (value is int)           // Direct type check
{
    int result = (int)value; // Direct cast
    Console.WriteLine(result);
}

The weaver automatically:

  1. ✅ Retargets local variables from OneOf<T1,T2>object
  2. ✅ Replaces From(value) with direct value (boxed if value type)
  3. ✅ Replaces IsT1 with value is T1
  4. ✅ Replaces AsT1() with (T1)value

📦 Installation

Simple - Just 2 packages:

<ItemGroup>
  <PackageReference Include="Fody" Version="6.8.0" PrivateAssets="all" />
  <PackageReference Include="AnAspect.OneOf" Version="0.1.1-alpha" />
</ItemGroup>

Note: The main package automatically includes the source generator and attributes as dependencies.

2. Add FodyWeavers.xml

<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
  <AnAspect.OneOf />
</Weavers>

3. Use OneOf

using OneOf;

// Reference types (optimal - no boxing)
OneOf<User, Admin> user = OneOf<User, Admin>.From(new User("Alice", 30));
if (user.IsT1)
{
    var u = user.AsT1();
    Console.WriteLine($"User: {u.Name}, Age: {u.Age}");
}

// Value types (boxing required, but still efficient)
OneOf<int, string> value = OneOf<int, string>.From(42);
if (value.IsT1)
{
    Console.WriteLine($"Got int: {value.AsT1()}");
}

// Mixed types
OneOf<Point, string> mixed = OneOf<Point, string>.From(new Point(10, 20));
if (mixed.IsT1)
{
    var p = mixed.AsT1();
    Console.WriteLine($"Point: ({p.X}, {p.Y})");
}

⚡ Performance Characteristics

Memory Usage:

  • Reference types: 1 object reference (8 bytes on x64) - OPTIMAL
  • Value types: 1 object reference + boxing overhead - acceptable trade-off
  • Discriminator overhead: ELIMINATED (0 bytes vs 1-8 bytes in traditional)

Performance:

  • Type checking: Direct is operator (no discriminator comparison)
  • Value extraction: Direct cast (no wrapper unwrapping)
  • Creation: Direct reference/box (no wrapper allocation for ref types)

🏗️ Architecture

Components:

  1. OneOf.Attributes: Marker attribute for Fody detection
  2. OneOf.SourceGenerator: Generates OneOf<T1,T2> class at compile time
  3. OneOf.Fody: IL weaver that transforms the generated code
  4. OneOf.Analyzers (TODO): Compile-time warnings and best practices

Supported Patterns:

  • From(T) - Create OneOf from value
  • IsT1, IsT2 - Type checking
  • AsT1(), AsT2() - Value extraction with exception on mismatch
  • ✅ Implicit conversions from T1/T2
  • ⚠️ TryGetT1(out T) - Not yet optimized (use Is/As pattern instead)

🎓 Best Practices

DO:

  • ✅ Use reference types in OneOf when possible (zero boxing overhead)
  • ✅ Use Is/As pattern for best performance
  • ✅ Store OneOf in local variables for optimal weaving

AVOID:

  • ⚠️ Don't use TryGet methods (not yet weaved - use Is/As instead)
  • ⚠️ Be aware of boxing overhead with value types
  • ⚠️ Debugger shows object after weaving (not OneOf<T1,T2>)

📋 Current Limitations

  • Only OneOf<T1, T2> supported (T3, T4, etc. TODO)
  • TryGet methods not yet rewritten (use Is/As pattern)
  • Fields/parameters/returns not yet retargeted to object (locals only)
  • No custom serializer support yet

🔬 Technical Details

IL Transformation Examples:

Before:

ldloc.0          // Load OneOf<int,string> local
call get_IsT1    // Call IsT1 property
brfalse.s IL_000d
ldloc.0
call AsT1        // Call AsT1 method

After:

ldloc.0          // Load object local
isinst int32     // Direct type check
brfalse.s IL_000d
ldloc.0
unbox.any int32  // Direct unbox

🧪 Testing

Run tests:

dotnet test tests/OneOf.Tests/OneOf.Tests.csproj

Run benchmarks:

dotnet run --project tests/OneOf.Benchmarks/OneOf.Benchmarks.csproj -c Release -- --manual

📝 License

MIT License - See LICENSE file for details.

🤝 Contributing

Contributions welcome! See CONTRIBUTING.md for guidelines.

GitHub: https://github.com/Pouria7/AnAspect.OneOf

🎯 Roadmap

  • Support OneOf<T1, T2, T3, ..., T8>
  • Implement TryGet IL rewriting
  • Retarget fields/parameters/returns to object
  • Custom serialization support
  • Roslyn analyzers for best practices
  • NuGet packaging
  • Documentation site

⚠️ Disclaimer

This is an experimental project demonstrating advanced IL weaving techniques. Use in production at your own risk. The trade-off is:

  • Gain: Significant memory savings, zero-cost abstractions
  • Cost: Boxing for value types, IL weaving complexity, debugger experience

For most applications, the memory savings and performance gains make this trade-off worthwhile, especially when using reference types.

About

A high-performance discriminated union library for C#

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors