Type implements ISerializable and GetObjectData but has no serialization constructor

ID

vbnet.correctness.implement_serialization_constructors

Severity

low

Remediation Complexity

medium

Remediation Risk

low

Remediation Effort

medium

Resource

Serialization

Language

VB.NET

Description

Reports a class that names ISerializable on its Implements clause - bare or namespace-qualified - and declares GetObjectData, but declares no serialization constructor Sub New(info As SerializationInfo, context As StreamingContext). The constructor is accepted with either bare or fully qualified parameter types, at any accessibility, and wherever it sits in the type body. Types that declare no GetObjectData are left to the rule that reports the missing interface member.

Rationale

Custom serialization is two matched halves, and the interface only declares one of them. GetObjectData writes the state; the constructor taking SerializationInfo and StreamingContext is what reads it back, and because it is a constructor the interface cannot require it. The compiler therefore accepts a type that can be serialized and never deserialized. The failure is deferred to run time and to the direction of travel: writing the object works, so the code that produced the stream looks correct, and the exception - SerializationException, reporting that the constructor was not found - is raised in whatever process reads it back. That is typically not the process that has the bug, and often not even the same deployment: a warm cache, a session hand-off between servers, a queued message, or a state file written by yesterday’s run. Tests miss it for the same reason, since a test that exercises GetObjectData alone passes. When the type is a base class the effect is inherited: every derived type is undeserializable too, and each one needs its own constructor because constructors are not inherited.

The following code illustrates the pattern detected by this rule:

Namespace Acme.Orders

    ' FLAGGED: Type implements ISerializable and GetObjectData but has no serialization constructor
    <Serializable>
    Public Class OrderSnapshot
        Implements ISerializable

        Private orderId As String
        Private total As Decimal

        Public Sub New(ByVal orderId As String, ByVal total As Decimal)
            Me.orderId = orderId
            Me.total = total
        End Sub

Remediation

Add a constructor with the signature Sub New(info As SerializationInfo, context As StreamingContext) that reads back, under the same names, every value GetObjectData writes, and call MyBase.New(info, context) first when the base type also implements ISerializable. Make it Protected on an unsealed type so derived types can chain to it, and Private on a NotInheritable type. Keep it beside GetObjectData and update both together whenever a field is added, since nothing checks that the two agree.

' Before: writes its state, cannot be read back
<Serializable>
Public Class OrderSnapshot
    Implements ISerializable

    Public Sub GetObjectData(info As SerializationInfo, context As StreamingContext) _
        Implements ISerializable.GetObjectData
        info.AddValue("orderId", orderId)
    End Sub
End Class

' After
<Serializable>
Public Class OrderSnapshot
    Implements ISerializable

    Protected Sub New(info As SerializationInfo, context As StreamingContext)
        orderId = info.GetString("orderId")
    End Sub

    Public Sub GetObjectData(info As SerializationInfo, context As StreamingContext) _
        Implements ISerializable.GetObjectData
        info.AddValue("orderId", orderId)
    End Sub
End Class

Configuration

This detector does not need any configuration.