Summary
EPC42 (DataContractSerializableMemberAnalyzer) does not report [DataMember]s whose type is a C# union. This is a deliberate gap: the analyzer builds against Microsoft.CodeAnalysis.CSharp 4.13.0, which cannot parse the union syntax, so the feature cannot be tested end-to-end today.
Why this matters
Unions are a worse failure mode than the IPAddress case EPC42 already covers. Instead of throwing, DataContractSerializer silently drops the payload.
Verified empirically with SDK 11.0.100-preview.6.26359.118 targeting net11.0:
public record Circle(double Radius);
public record Square(double Side);
public union Shape(Circle, Square); // case types go in the header list
[DataContract]
public class Config
{
[DataMember]
public Shape Value { get; set; }
}
var s = new DataContractSerializer(typeof(Config));
s.WriteObject(stream, new Config { Value = new Circle(42.0) });
Output — no exception, and the Circle is gone:
<Config xmlns="http://schemas.datacontract.org/2004/07/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><Value/></Config>
Round-tripping through ReadObject returns an empty union.
Root cause
Reflection over a compiled union shows the shape the compiler emits:
| Aspect |
Value |
| Kind |
sealed struct (BaseType = System.ValueType) |
| Interfaces |
System.Runtime.CompilerServices.IUnion |
| Attributes |
System.Runtime.CompilerServices.UnionAttribute |
| Constructors |
one per case type — (Circle), (Square); no parameterless one |
| State |
a private field <Value>k__BackingField of type object |
| Public surface |
a get-only Value property of type object |
DataContractSerializer's POCO contract serializes public read/write properties and public fields only. A union exposes neither, so it has zero serializable members — hence the empty element and the silent data loss.
Note also that EPC42's existing checks cannot catch this incidentally: a union is a public value type, so it satisfies both "is public" and "has a parameterless constructor".
Proposed implementation
In DataContractSerializableMemberAnalyzer.IsDataContractSerializable, after the existing [DataContract] / [CollectionDataContract] / IsSerializable / ISerializable / IXmlSerializable checks, report the member when its type is a union:
private static bool IsUnion(INamedTypeSymbol type, SerializationTypes knownTypes)
{
// Both are checked so that detection does not depend on a single marker.
return HasAttribute(type, knownTypes.UnionAttribute) || Implements(type, knownTypes.IUnion);
}
resolving System.Runtime.CompilerServices.UnionAttribute and System.Runtime.CompilerServices.IUnion through the existing WellKnownTypeProvider (both full names verified against a compiled union).
Placing the check after the explicit attribute checks keeps [DataContract] on a union working as an opt-in escape hatch.
Suggested message, distinct from the existing two reasons ("is not public" / "has no parameterless constructor"):
type 'Shape' is a union type whose state is not visible to DataContractSerializer: such members are silently serialized as empty and the data is lost
The existing generic-argument/array/Nullable<T> unwrapping already covers List<Shape>, Shape[] and Shape? for free.
Blocked on
Bumping Microsoft.CodeAnalysis.CSharp (see src/Directory.Packages.props, currently 4.13.0) to a version whose parser understands union. Until then a test can only hand-declare the compiler-emitted shape (a struct implementing a stub IUnion), which validates the symbol-level logic but not the real syntax.
Test cases to add once unblocked
- warn on a plain union-typed
[DataMember];
- warn on
List<Shape>, Shape[], Shape?;
- detection via the interface alone, and via the attribute alone;
- no warning when the union is explicitly marked with
[DataContract];
- no warning for a non-union struct with a get-only
object property (guards against over-matching);
- no warning for the union's case types themselves — they are ordinary types;
- no warning when the member is not a
[DataMember] or is marked [IgnoreDataMember].
Related
Also worth considering under the same umbrella: any type whose state is exposed exclusively through get-only members hits the same silent-data-loss path. Unions are just the case where the language guarantees that shape.
Summary
EPC42 (
DataContractSerializableMemberAnalyzer) does not report[DataMember]s whose type is a C#union. This is a deliberate gap: the analyzer builds againstMicrosoft.CodeAnalysis.CSharp4.13.0, which cannot parse theunionsyntax, so the feature cannot be tested end-to-end today.Why this matters
Unions are a worse failure mode than the
IPAddresscase EPC42 already covers. Instead of throwing,DataContractSerializersilently drops the payload.Verified empirically with SDK
11.0.100-preview.6.26359.118targetingnet11.0:Output — no exception, and the
Circleis gone:Round-tripping through
ReadObjectreturns an empty union.Root cause
Reflection over a compiled union shows the shape the compiler emits:
BaseType=System.ValueType)System.Runtime.CompilerServices.IUnionSystem.Runtime.CompilerServices.UnionAttribute(Circle),(Square); no parameterless one<Value>k__BackingFieldof typeobjectValueproperty of typeobjectDataContractSerializer's POCO contract serializes public read/write properties and public fields only. A union exposes neither, so it has zero serializable members — hence the empty element and the silent data loss.Note also that EPC42's existing checks cannot catch this incidentally: a union is a public value type, so it satisfies both "is public" and "has a parameterless constructor".
Proposed implementation
In
DataContractSerializableMemberAnalyzer.IsDataContractSerializable, after the existing[DataContract]/[CollectionDataContract]/IsSerializable/ISerializable/IXmlSerializablechecks, report the member when its type is a union:resolving
System.Runtime.CompilerServices.UnionAttributeandSystem.Runtime.CompilerServices.IUnionthrough the existingWellKnownTypeProvider(both full names verified against a compiled union).Placing the check after the explicit attribute checks keeps
[DataContract]on a union working as an opt-in escape hatch.Suggested message, distinct from the existing two reasons ("is not public" / "has no parameterless constructor"):
The existing generic-argument/array/
Nullable<T>unwrapping already coversList<Shape>,Shape[]andShape?for free.Blocked on
Bumping
Microsoft.CodeAnalysis.CSharp(seesrc/Directory.Packages.props, currently 4.13.0) to a version whose parser understandsunion. Until then a test can only hand-declare the compiler-emitted shape (a struct implementing a stubIUnion), which validates the symbol-level logic but not the real syntax.Test cases to add once unblocked
[DataMember];List<Shape>,Shape[],Shape?;[DataContract];objectproperty (guards against over-matching);[DataMember]or is marked[IgnoreDataMember].Related
Also worth considering under the same umbrella: any type whose state is exposed exclusively through get-only members hits the same silent-data-loss path. Unions are just the case where the language guarantees that shape.