Serialization Event Handler Signature

ID

csharp.serialization_event_handler_signature

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

CSharp

Tags

api-contract, attributes, reliability, serialization

Description

Reports a method carrying OnSerializing, OnSerialized, OnDeserializing or OnDeserialized whose signature is not the one the runtime looks for: returning void, taking exactly one StreamingContext parameter, on an instance and not generic. Accessibility does not matter — public, internal and private handlers are all discovered.

Rationale

Serialization callbacks are matched by shape, not by registration. When a type is serialized or restored, the runtime scans it for methods carrying one of the four attributes and having the exact required signature. A method that carries the attribute but differs in any respect is not a candidate, and the runtime does not consider that worth mentioning: no exception, no warning, no log line.

So the failure is a hook that never fires. That hook was written for a reason — reconnect a field marked as not serialized, rebuild a cached value, restore an invariant the constructor normally establishes — and none of it happens. The object graph comes back structurally intact and semantically wrong, and the symptom appears far away from the type that owns the handler.

The requirements each have a reason behind them:

  • Returns void. There is no caller to receive a result. A return type is a sign the method was written for something else and the attribute was added to it later.

  • Exactly one StreamingContext parameter. That argument carries the reason for the operation, and it is the only thing the runtime is prepared to pass.

  • Instance, non-generic. The callback runs against the object being serialized, so there is nothing for a static or generic handler to act on.

Accessibility is not one of the requirements: the runtime discovers these callbacks reflectively over public and non-public members alike, internal included, so a public or internal handler with the right shape runs exactly as an equivalent private one would.

Both spellings of every attribute are recognised, qualified or not, since C# allows the Attribute suffix to be omitted at the use site.

using System;
using System.Runtime.Serialization;

[Serializable]
public class Session
{
    private string token;

    [OnDeserialized]
    internal void OnDeserializedMethod(StreamingContext context) // OK — accessibility is irrelevant
    {
        token = "restored";
    }

    [OnSerializing]
    private void Prepare()                              // FLAW — no StreamingContext parameter
    {
        token = null;
    }

    [OnDeserializing]
    private void Reset(StreamingContext context)        // OK — void, one context parameter
    {
        token = string.Empty;
    }

    public void Refresh(StreamingContext context)       // OK — no callback attribute at all
    {
        token = null;
    }
}

Remediation

Give the handler the required signature: void Name(StreamingContext context) on an instance, non-generic method. Drop any return value, remove static and any type parameters. Accessibility does not need to change — leave it as public, internal or private, whichever fits the type’s own design.

Once the signature is corrected the handler starts running, which may be the first time its body has ever executed: review it as new code rather than as code that has been working all along.