Skip to content
PYD ENTERPRISES

.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

PolicyContracts.csC#
1[DataContract(Namespace = "urn:acme:policy:2014")]
2[KnownType(typeof(AutoCoverage))]
3[KnownType(typeof(HomeCoverage))]
4public class Coverage : IExtensibleDataObject
5{
6 [DataMember(Order = 1, IsRequired = true)]
7 public string CoverageCode { get; set; }
8
9 [DataMember(Order = 2, EmitDefaultValue = false)]
10 public decimal Deductible { get; set; } // 0 omitted
11
12 [DataMember(Order = 3)]
13 public bool IsWaived { get; set; }
14
15 public ExtensionDataObject ExtensionData { get; set; }
16}
17
18[ServiceContract(Namespace = "urn:acme:policy:2014")]
19public interface IPolicyService
20{
21 [OperationContract]
22 [FaultContract(typeof(PolicyLockedFault))]
23 Coverage[] GetCoverages(string policyNo);
24}

Modern: Versioned OpenAPI 3.1 schemas with discriminated oneOf

openapi.components.yamlYAML
1Coverage:
2 type: object
3 required: [coverageCode, coverageType]
4 properties:
5 coverageCode: { type: string }
6 coverageType: { enum: [auto, home] }
7 deductible:
8 type: number
9 default: 0
10 description: >
11 Legacy omitted 0 (EmitDefaultValue=false);
12 absent and 0 are equivalent.
13 isWaived: { type: boolean }
14 discriminator:
15 propertyName: coverageType
16 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

openapi.paths.yamlYAML
1/policies/{policyNo}/coverages:
2 get:
3 operationId: getCoverages
4 summary: IPolicyService.GetCoverages (urn:acme:policy:2014)
5 responses:
6 '200':
7 content:
8 application/json:
9 schema:
10 type: array
11 items: { $ref: '#/components/schemas/Coverage' }
12 '423':
13 description: FaultContract PolicyLockedFault
14 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.

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.