Skip to content

Comments

Add PolyType support for efficient serialization integration#835

Merged
SteveDunn merged 5 commits intoSteveDunn:mainfrom
AArnott:copilot/fix-6f261f1c-67b8-49c6-a33b-c3e4980c812b
Sep 16, 2025
Merged

Add PolyType support for efficient serialization integration#835
SteveDunn merged 5 commits intoSteveDunn:mainfrom
AArnott:copilot/fix-6f261f1c-67b8-49c6-a33b-c3e4980c812b

Conversation

@AArnott
Copy link
Contributor

@AArnott AArnott commented Aug 27, 2025

This PR implements support for PolyType integration as requested in issue #834. The implementation conditionally emits additional code when PolyType.TypeShapeAttribute is available in the compilation, enabling seamless integration between Vogen value objects and PolyType-based serialization libraries like Nerdbank.MessagePack.

Problem Statement

PolyType is a high-performance type modeling library that uses source generation to expose type graphs at runtime. However, PolyType's source generator cannot see code emitted by Vogen's source generator, creating a compatibility issue. This forces users to either:

  • Use separate assemblies for data models and serialization (suboptimal)
  • Accept inefficient serialized schemas with unnecessary wrapper layers

Solution

When PolyType.TypeShapeAttribute is detected in the compilation, Vogen now automatically generates:

  1. TypeShape attribute on the partial value object declaration
  2. PolyTypeMarshaler nested class that implements IMarshaler<T, TUnderlying>

This enables PolyType to efficiently serialize/deserialize the underlying primitive value directly, bypassing the wrapper type and producing optimal schemas.

Example

For a value object like:

[ValueObject<int>]
public partial struct CustomerId { }

When PolyType is available, Vogen now generates:

[global::PolyType.TypeShapeAttribute(Marshaler = typeof(PolyTypeMarshaler), Kind = global::PolyType.TypeShapeKind.None)]
public partial struct CustomerId
{
    [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
    public class PolyTypeMarshaler : global::PolyType.IMarshaler<CustomerId, int>
    {
        int global::PolyType.IMarshaler<CustomerId, int>.Marshal(CustomerId value) => value.Value;
        CustomerId global::PolyType.IMarshaler<CustomerId, int>.Unmarshal(int value) => From(value);
    }
}

Implementation Details

  • Conditional Generation: Code only appears when PolyType.TypeShapeAttribute is found in the compilation
  • Zero Configuration: No user flags or attributes required - automatically detects PolyType availability
  • Universal Support: Works with all Vogen type declarations (class, struct, record class, record struct)
  • Type Safety: Uses proper global:: prefixes and follows existing Vogen patterns
  • Format Agnostic: Works with any PolyType-based serialization library, not just MessagePack

Benefits

  • Enables high-performance serialization with PolyType without requiring separate assemblies
  • Produces optimal serialized schemas by marshaling the underlying primitive directly
  • Supports all current and future PolyType use cases
  • Maintains full backward compatibility - no breaking changes

This resolves the core incompatibility between PolyType and Vogen source generators, enabling developers to use both libraries together seamlessly for efficient, type-safe serialization.


🤖 This PR was prepared in whole or in part by GitHub Copilot (over at AArnott#1). I copied it here so that it could be taken by the upstream repo.

Copilot AI and others added 5 commits August 27, 2025 15:49
- Add TypeShapeAttribute detection to VogenKnownSymbols
- Add utility methods for generating PolyType attribute and marshaler
- Integrate PolyType generation into all four generators (Class, Struct, RecordClass, RecordStruct)
- PolyType code only generated when PolyType.TypeShapeAttribute is available in compilation

Co-authored-by: AArnott <3548+AArnott@users.noreply.github.com>
- Add snapshot tests for PolyType functionality
- Create validation script demonstrating expected behavior
- Tests verify conditional generation based on PolyType availability

Co-authored-by: AArnott <3548+AArnott@users.noreply.github.com>
Removed unnecessary whitespace and comments in PolyTypeTests.cs
@aradalvand
Copy link
Contributor

aradalvand commented Aug 27, 2025

I'm personally still not clear on how this would work. You said:

Oh yes, you're right. Marshalers work as a workaround for users, but if Vogen emits them directly, it still would only help users that define their data types in a separate assembly/project from the code that serializes them.

I don't get it. If the TypeShape attribute only works if the PolyType's source generator can see the annotation (so that it generates some partial code in addition to the original declaration), how could this work at all, even if the types were in a separate dependency?

Or is it supposed to work with the reflection-based shape provider, for example?

@AArnott
Copy link
Contributor Author

AArnott commented Aug 27, 2025

The marshaler is required to avoid emitting this schema:

{
    "Id": {
        "Value": 123
    },
    "Name": "Some Person"
}

When we should be emitting this schema:

{
    "Id": 123,
    "Name": "Some Person"
}

And yes, this marshaler added here will work anywhere so long as the assembly uses ReflectionTypeShapeProvider.
This marshaler will also work with the (more preferred) source generation type shape provider if that usage happens in another assembly.

So while this PR doesn't solve the dual source generator problem, there are workarounds for that. But this PR does solve a real schema problem.

@AArnott
Copy link
Contributor Author

AArnott commented Aug 27, 2025

The only arguable takeback is that users can hand author this new code today in their own projects, thereby getting the PolyType source generator to work, all in one assembly. But handwriting all this code seems like too high a price for that convenience, and given that without the marshaler, the schema is wrong-by-default, I think this is the lesser of two evils.

@aradalvand
Copy link
Contributor

And yes, this marshaler added here will work anywhere so long as the assembly uses ReflectionTypeShapeProvider.
This marshaler will also work with the (more preferred) source generation type shape provider if that usage happens in another assembly.

Gotcha. That makes perfect sense now.

@aradalvand
Copy link
Contributor

aradalvand commented Aug 27, 2025

The only other piece of feedback I'd have (and this is where we'd need @SteveDunn to chime in) is that we probably want to make this a configurable option in [VogenDefaults(...)], so as to be consistent with other "converter"-type things (e.g. JSON converters, etc.), as opposed to it being automatic.

@AArnott
Copy link
Contributor Author

AArnott commented Aug 27, 2025

we probably want to make this a configurable option

Ah, interesting.
Copilot just did what I told it. So if Steve agrees with you, I'll direct Copilot to update the PR.

@SteveDunn SteveDunn merged commit c6c6385 into SteveDunn:main Sep 16, 2025
5 of 6 checks passed
@SteveDunn
Copy link
Owner

SteveDunn commented Sep 16, 2025

Thank you for the PR! I considered whether this should be behind a feature flag like the the other conversion generators. I came to the conclusion that it doesn't; unlike other conversions, if the attribute exists, then the user probably wants the marshaler, and if they don't, then there's no significant overhead, nor additional dependencies that are needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants