.NET anti-pattern
WCF DataContract versioning
WCF service contracts whose wire format depends on DataMember attributes, serializer ordering rules, KnownType declarations and binding configuration, not just on the C# class shape.
- FAMILY
- .NET
- CUTOVER RISK
- Medium
- Adds migration effort and contract risk, but behavior is visible in the code once you look.
- MODERN MAPPING
- Versioned OpenAPI 3.1 schemas with discriminated oneOf
- GENERATED ARTIFACT
- openapi.paths.yaml
Before and after
legacy source → modern mapping
Legacy: exhibits the pattern
1[DataContract(Namespace = "urn:acme:policy:2014")]2[KnownType(typeof(AutoCoverage))]3[KnownType(typeof(HomeCoverage))]4public class Coverage : IExtensibleDataObject5{6 [DataMember(Order = 1, IsRequired = true)]7 public string CoverageCode { get; set; }89 [DataMember(Order = 2, EmitDefaultValue = false)]10 public decimal Deductible { get; set; } // 0 omitted1112 [DataMember(Order = 3)]13 public bool IsWaived { get; set; }1415 public ExtensionDataObject ExtensionData { get; set; }16}1718[ServiceContract(Namespace = "urn:acme:policy:2014")]19public interface IPolicyService20{21 [OperationContract]22 [FaultContract(typeof(PolicyLockedFault))]23 Coverage[] GetCoverages(string policyNo);24}
Modern: Versioned OpenAPI 3.1 schemas with discriminated oneOf
1Coverage:2 type: object3 required: [coverageCode, coverageType]4 properties:5 coverageCode: { type: string }6 coverageType: { enum: [auto, home] }7 deductible:8 type: number9 default: 010 description: >11 Legacy omitted 0 (EmitDefaultValue=false);12 absent and 0 are equivalent.13 isWaived: { type: boolean }14 discriminator:15 propertyName: coverageType16 oneOf:17 - $ref: '#/components/schemas/AutoCoverage'18 - $ref: '#/components/schemas/HomeCoverage'
Why it is dangerous in modernization
A WCF DataContract looks like a plain C# class with attributes, but the attributes decide the contract. DataContractSerializer orders members by base class first, then by the Order value, then alphabetically, so renaming a property can move it on the wire. EmitDefaultValue = false omits members that hold their default, so a consumer cannot tell zero from absent. IsRequired = true makes deserialization fail when a member is missing. KnownType attributes declare which subclasses can appear where the base type is declared, and IExtensibleDataObject keeps unknown members for round-tripping between contract versions. Bindings in web.config (basicHttpBinding, wsHttpBinding, netTcpBinding) decide transport, security and message size limits. A port that regenerates DTOs from the C# classes keeps the names and loses the versioning behavior that consumers depend on.
How PYD ENTERPRISES detects it
PYD ENTERPRISES reads ServiceContract, OperationContract, DataContract, DataMember, KnownType and FaultContract attributes from the C# or VB.NET syntax tree, and the system.serviceModel section of the configuration files for endpoints and bindings. It computes the effective wire order and name of every member, flags members with EmitDefaultValue false whose default is a meaningful business value (0, false, an enum's first member), and resolves polymorphic types from KnownType and ServiceKnownType declarations. Within operation implementations, it follows which members are read and written, and which FaultException<T> types are thrown on which paths.
Maps to: Versioned OpenAPI 3.1 schemas with discriminated oneOf
Each DataContract becomes a JSON Schema component with explicit required properties derived from IsRequired and from how the implementation uses the member. KnownType hierarchies become oneOf with a discriminator property. FaultContracts become problem details error responses. Contract versions identified by namespace become versioned components, so consumers on older versions are visible in the specification rather than in incident tickets.
Generated specification fragment
1/policies/{policyNo}/coverages:2 get:3 operationId: getCoverages4 summary: IPolicyService.GetCoverages (urn:acme:policy:2014)5 responses:6 '200':7 content:8 application/json:9 schema:10 type: array11 items: { $ref: '#/components/schemas/Coverage' }12 '423':13 description: FaultContract PolicyLockedFault14 content:15 application/problem+json:16 schema: { $ref: '#/components/schemas/Problem' }
Every generated element links back to the source file and line range shown in the legacy pane. Review and sign off before building against it.
Wire order is not declaration order
DataContractSerializer writes base class members before derived class members. Within a class, members without an Order come first, sorted alphabetically, followed by members with an Order, ascending by Order and then alphabetically. Consumers using strict XML schema validation, which is common with Java clients generated from the WSDL, reject messages whose element order changed. PYD ENTERPRISES computes the effective order and records it in the generated contract, so a SOAP facade kept during migration emits exactly what consumers expect.
Defaults that mean something
EmitDefaultValue = false is often applied to whole classes to shrink messages. On a decimal deductible it means a zero deductible is never sent, and consumers infer zero from absence. On an enum it means the first member is never sent. On a bool it means false is never sent. Most of the time that is harmless; occasionally a consumer treats absence as unknown and applies its own default. The engine lists every such member with the consumer-facing default, so the JSON contract states it explicitly instead of relying on each client to guess.
Bindings carry operational rules
- maxReceivedMessageSize and reader quotas cap request size; the new API needs equivalent limits.
- Security mode and client credential type (Windows, certificate, username) decide who can call; map them to the new authentication scheme.
- Reliable sessions and transaction flow on wsHttpBinding imply guarantees that a plain REST endpoint does not give.
Each is extracted from configuration per endpoint and listed beside the operation it applies to.
Related patterns
Usually found in the same codebase.
- SOAPMedium
SOAP and WSDL schema coupling
Maps to OpenAPI 3.1 with JSON Schema 2020-12 and problem details
- Java EEMedium
EJB home and remote interfaces
Maps to Coarse-grained REST contract with DTOs
- .NETHigh
WebForms ViewState and code-behind logic
Maps to SPA front end over a stateless API that rereads authoritative data
- Next in catalog
PL/SQL row-by-row cursor
Find every WCF DataContract versioning in your codebase.
Run PYD ENTERPRISES in your private VPC and get each occurrence with file, line range, modern mapping and equivalence fixtures, ready for your architects to review.