Vogen Help

Conversion Mechanisms

Overview

When converting between value objects and other types, you have several options with different tradeoffs. Understanding when to use each mechanism ensures your code is both type-safe and maintainable.

Quick Reference Matrix

Mechanism

Best For

Type Safety

Framework Friendly

Performance

Validation

IConvertible

Dynamic runtime conversion

⭐⭐

Yes (LINQ-to-SQL, ORMs)

⭐⭐

N/A

Explicit Cast

Domain logic, explicit intent

⭐⭐⭐

Limited

⭐⭐⭐

Yes

Implicit Cast

⚠️ Not recommended

Limited

⭐⭐⭐

No

TypeConverter

Framework binding, ASP.NET routes

⭐⭐

⭐⭐⭐

⭐⭐

Yes

JSON (STJ/NSJ)

API serialization

⭐⭐⭐

⭐⭐⭐

⭐⭐⭐

Yes

EF Core Converter

Database persistence

⭐⭐⭐

⭐⭐⭐

⭐⭐

Via converter

BSON/MessagePack

Format-specific serialization

⭐⭐⭐

⭐⭐⭐

⭐⭐⭐

Yes

IConvertible – Dynamic Runtime Conversion

For detailed information, see IConvertible

When to Use IConvertible

Use IConvertible when:

  • You need dynamic type conversion with the target type determined at runtime

  • Integrating with frameworks that expect IConvertible (some ORMs, reflection utilities)

  • You need Convert.ChangeType() to work seamlessly with your value objects

  • Working with legacy code that relies on runtime type conversion

When NOT to Use

Avoid relying solely on IConvertible when:

  • You need compile-time type safety (prefer explicit casting)

  • The conversion requires validation (prefer TypeConverter or explicit methods)

  • You want clear intent in your code (explicit methods are more readable)

Example: ORM Dynamic Mapping

[ValueObject<int>] public partial struct OrderId { } [ValueObject<string>] public partial struct CustomerName { } // Framework receives target type at runtime public class DataMapper { public object MapValue(object source, Type targetType) { // IConvertible enables this to work with value objects if (source is IConvertible convertible) { return Convert.ChangeType(convertible, targetType); } throw new NotSupportedException(); } } var orderId = OrderId.From(123); var asString = new DataMapper().MapValue(orderId, typeof(string)); // "123"

Explicit Casting - Type-Safe Domain Logic

When to Use Casting

Use explicit casting when:

  • You need compile-time type safety with clear intent

  • Converting as part of domain logic (e.g., (int)customerId)

  • You want to prevent accidental conversions between different value objects

  • Performance is critical and you're converting frequently

Configuration

[ValueObject<int>( toPrimitiveCasting: CastOperator.Explicit, // Default fromPrimitiveCasting: CastOperator.Explicit // Default )] public partial struct UserId { } var userId = UserId.From(42); int raw = (int)userId; // Explicit cast: userId → int UserId restored = (UserId)raw; // Explicit cast: int → userId

Example: Domain Logic with Type Safety

public class OrderService { public void ProcessOrder(OrderId orderId, CustomerId customerId) { // Explicit casts are intentional and clear int orderNum = (int)orderId; int custNum = (int)customerId; var order = _database.GetOrder(orderNum); order.CustomerId = custNum; } }

While you can enable implicit casting, it is not recommended because:

  1. Reduces type safety - Conversions can happen accidentally

  2. Violates implicit conversion rules - implicit should never throw, but value object conversion validates

  3. Ambiguity between value objects - implicit operator int makes it unclear which value object you're converting

// ⚠️ NOT RECOMMENDED: [ValueObject<int>( toPrimitiveCasting: CastOperator.Implicit, // Risky! fromPrimitiveCasting: CastOperator.Implicit // Risky! )] public partial struct UserId { } UserId userId = 42; // Looks innocent but bypasses validation/intent

TypeConverter – Framework Integration

When to Use TypeConverters

Use TypeConverter when:

  • ASP.NET Core parameter binding in routes, query strings, or model binding

  • WPF data binding in XAML

  • Data grids that convert cell values to/from strings

  • Any framework that uses the .NET type conversion infrastructure

How It Works

Vogen generates a TypeConverter that:

  1. Converts value objects to/from strings

  2. Validates during conversion (respects your validation rules)

  3. Integrates with ASP.NET Core's [FromRoute], [FromQuery] attributes

  4. Works with WPF bindings and other reflection-based frameworks

Example: ASP.NET Core Routes

[ValueObject<int>] public partial struct UserId { } [ApiController] [Route("api/[controller]")] public class UsersController : ControllerBase { [HttpGet("{userId}")] public IActionResult GetUser(UserId userId) // TypeConverter converts route param { // userId is type-safe return Ok(_userService.GetUser(userId)); } [HttpGet("search")] public IActionResult Search([FromQuery] UserId? userId) { // Query string "?userId=42" converted via TypeConverter return Ok(/* ... */); } }

Example: WPF Data Binding

// XAML <TextBox Text="{Binding CustomerId}"/> // C# ViewModel [ValueObject<string>] public partial class CustomerId { } public class CustomerViewModel { public CustomerId CustomerId { get; set; } // TypeConverter handles string ↔ CustomerId }

JSON Serialization - API & Data Exchange

JSON serialization is handled through different converters based on your needs.

Advantages:

  • Default (included automatically)

  • Native .NET support, excellent performance

  • AOT-friendly with source-generated factories

  • Serializes primitive directly: {"price": 99.99} instead of {"price": {"value": 99.99}}

Usage:

[ValueObject<decimal>] public partial struct Price { } var price = Price.From(99.99m); // Serialization string json = JsonSerializer.Serialize(price); // "99.99" // Deserialization var restored = JsonSerializer.Deserialize<Price>("99.99");

Newtonsoft.Json (JSON.NET) – Legacy Support

If you're using Newtonsoft.Json in your project:

[ValueObject<string>(conversions: Conversions.NewtonsoftJson)] public partial class BookTitle { } var title = BookTitle.From("Clean Code"); // Serialization string json = JsonConvert.SerializeObject(title); // "Clean Code"

When to choose:

  • You already depend on Newtonsoft.Json

  • You need specific Newtonsoft features or settings

  • Legacy projects that use JSON.NET extensively

JSON Schema Best Practice

Both STJ and Newtonsoft serialize the primitive directly, which is the correct design:

[ValueObject<string>] public partial class OrderId { } var order = new Order { Id = OrderId.From("ORD-123"), Amount = 99.99m }; // JSON representation { "id": "ORD-123", // ← Direct value, not { "value": "ORD-123" } "amount": 99.99 }

This approach:

  • ✅ Matches schema expectations

  • ✅ Provides clean API contracts

  • ✅ Improves debuggability

  • ✅ Reduces network payload

Database Integration

Entity Framework Core

[ValueObject<int>(conversions: Conversions.EfCoreValueConverter)] public partial struct CustomerId { } public class Order { public int Id { get; set; } public CustomerId CustomerId { get; set; } } // In DbContext protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Order>() .Property(o => o.CustomerId) .HasConversion<CustomerId.EfCoreValueConverter>(); }

When to use: Primary database persistence with EF Core.

Dapper

[ValueObject<string>(conversions: Conversions.DapperTypeHandler)] public partial class ProductCode { } // Register at startup SqlMapper.AddTypeHandler(new ProductCode.DapperTypeHandler()); // Now Dapper queries handle conversion automatically var product = connection.QuerySingle<Product>( "SELECT * FROM Products WHERE Code = @code", new { code = ProductCode.From("ABC-123") });

When to use: Lightweight micro-ORMs with manual SQL queries.

LINQ to DB

[ValueObject<Guid>(conversions: Conversions.LinqToDbValueConverter)] public partial struct OrderId { } using (var db = new MyDataContext()) { var orders = db.Orders .Where(o => o.Id == orderId) // LinqToDB handles conversion .ToList(); }

When to use: LINQ to DB data access layer.

Specialized Formats

BSON (MongoDB)

[ValueObject<ObjectId>(conversions: Conversions.Bson)] public partial class CustomerId { } public class Customer { [BsonId] public CustomerId Id { get; set; } public string Name { get; set; } } // MongoDB driver handles conversion automatically var customer = collection.Find(c => c.Id == customerId).FirstOrDefault();

MessagePack

[ValueObject<int>(conversions: Conversions.MessagePack)] public partial struct UserId { } // MessagePack handles efficient serialization var bytes = MessagePackSerializer.Serialize(userId); var restored = MessagePackSerializer.Deserialize<UserId>(bytes);

Orleans (Distributed Systems)

[ValueObject<Guid>(conversions: Conversions.Orleans)] public partial struct GrainId { } public interface IUserGrain : IGrainWithGuidKey { Task<string> GetName(); } // Orleans handles serialization across grain boundaries var grain = grainFactory.GetGrain<IUserGrain>(grainId);

Decision Framework

When choosing a conversion mechanism, ask these questions in order:

  1. Are you converting at API boundaries?

    • Yes → Use JSON serialization (STJ/Newtonsoft) or appropriate format (BSON, MessagePack)

    • No → Continue to #2

  2. Is the conversion happening in domain logic?

    • Yes → Use explicit casting for type safety

    • No → Continue to #3

  3. Does your framework need to convert?

    • ASP.NET Core routes/binding → Use TypeConverter

    • Database persistence → Use EF/Dapper/LINQ-to-DB converter

    • Dynamic/reflection-based → Use IConvertible

    • Continue to #4

  4. Do you need dynamic runtime conversion?

    • Yes → Use IConvertible

    • No → Use explicit casting or custom methods

Advanced: Combining Mechanisms

You often use multiple mechanisms in the same application:

[ValueObject<int>(conversions: Conversions.SystemTextJson | Conversions.EfCoreValueConverter)] public partial struct CustomerId { } // ASP.NET Controller accepts TypeConverter-converted route param [HttpGet("{customerId}")] public async Task<IActionResult> GetCustomer(CustomerId customerId) { // EF Core uses the value converter in queries var customer = await _db.Customers .FirstOrDefaultAsync(c => c.Id == customerId); // JSON converter handles API response serialization return Ok(customer); }

See Also

Last modified: 05 March 2026