No Base Call Extending Object

ID

csharp.no_base_call_extending_object

Severity

high

Remediation Complexity

medium

Remediation Risk

low

Remediation Effort

medium

Resource

Code Smell

Language

CSharp

Tags

code_smell, equals, identity, inheritance

Description

Reports a call to base.Equals or base.GetHashCode inside an Equals or GetHashCode override of a class that extends object directly — one that declares no base class, or names only object or interfaces in its base list.

Not reported: a base call used as an if condition, which is a legitimate reference-equality fast path; plain delegation from an Equals override, which csharp.base_equals_reference covers; classes with a real base class, whose inherited implementation may be structural; and records and structs, whose inherited implementation is value-based.

Rationale

object.Equals and object.GetHashCode are defined on identity, not on content. The first returns true only for the very same instance; the second derives its value from that instance. Feeding either into an override cancels the override, because two distinct instances holding equal values can never satisfy it:

public class Point
{
    public int X;
    public int Y;

    public override bool Equals(object obj)
    {
        return base.Equals(obj) && ((Point)obj).X == X;   // FLAW - false for two equal Points
    }

    public override int GetHashCode()
    {
        return base.GetHashCode() ^ X;                    // FLAW - identity leaks into the hash
    }
}

public class Size
{
    public int Width;

    public override bool Equals(object obj)
    {
        if (base.Equals(obj)) return true;                // OK - reference-equality fast path
        return obj is Size other && other.Width == Width;
    }

    public override int GetHashCode() => Width;           // OK - derived from state
}

The hash case is the more damaging one. Two objects that compare equal are required to return the same hash, and an identity component breaks that contract, so a dictionary or set stops finding entries it holds — usually not at the line that caused it, and not for every run. The Equals case fails more visibly but just as completely: the override looks like it compares values and does not.

Remediation

Drop the base call and compare the fields that define the type’s identity, combining their hashes with HashCode.Combine. Keep the two members consistent: every field used in Equals should contribute to GetHashCode, and no field should contribute to the hash unless it is stable for the lifetime of the object. If the type really is compared by reference, delete the overrides and let the inherited behaviour stand; if it is a value, consider declaring it a record, which generates both members from its state.