No Cast Interface To Concrete

ID

csharp.no_cast_interface_to_concrete

Severity

high

Remediation Complexity

medium

Remediation Risk

low

Remediation Effort

medium

Resource

Code Smell

Language

CSharp

Tags

abstraction, cast, code_smell, coupling

Description

Reports a downcast from an interface reference to the concrete class or struct behind it, written either as (MyClass)value or as value as MyClass.

Rationale

A cast like this is a design signal: the code holds the interface because that is what it was handed, but it wants something the interface does not offer. The honest fix is to add the member to the interface, or to declare the concrete type in the signature so callers know what is required. Reaching past the abstraction at the point of use ties that line to one implementation, which is the coupling the interface existed to prevent.

It is also a failure waiting for a second implementation. The compiler accepts the downcast on trust; the moment a different implementation, a decorator or a test double travels the same path, (MyClass)value throws InvalidCastException. The as form is worse in one respect: it yields null instead of failing, so the error surfaces later and somewhere else.

Reporting is confined to types declared in the file under analysis, and only when the source resolves to an interface and the target to a class or struct. Interface-to-interface conversions, upcasts from a concrete type, and expressions whose type cannot be resolved are not reported.

public interface IShape
{
    double Area();
}

public class Circle : IShape
{
    public double Radius;
    public double Area() { return 3.14 * Radius * Radius; }
}

public interface ISolid : IShape
{
    double Volume();
}

public class Report
{
    public double RadiusOf(IShape shape)
    {
        Circle circle = (Circle)shape;      // FLAW — the abstraction lacks what the caller needs
        return circle.Radius;
    }

    public double VolumeOf(IShape shape)
    {
        ISolid solid = (ISolid)shape;       // OK, interface to interface
        return solid.Volume();
    }

    public double AreaOf(Circle circle)
    {
        IShape shape = circle;              // OK, no downcast is needed
        return shape.Area();
    }
}

Limitations

Both the interface and the concrete type it is cast down to must be declared in the file under analysis, because that resolution does not cross files. In a codebase organized around the usual interface/implementation split — the interface in one file, each implementation in its own — the two types this rule needs to see are rarely declared together, so most real casts of this shape fall outside what it can catch. A clean run says only that no such cast collided with a same-file declaration, not that the codebase has none.

Remediation

Add the member the caller needs to the interface, so no cast is required. Where the member belongs only to one implementation, take the concrete type as the parameter type and let the signature state the dependency. Where the code genuinely handles several shapes of value, use pattern matching (if (shape is Circle circle)) so the non-matching case is handled explicitly instead of throwing.