Static Abstract Interface Members in C#

For years, C# interfaces could only define instance members. If you wanted to write a generic method that called a static method — like a factory method or an operator — you were stuck. C# 11 introduced static abstract interface members, and they change what generic code can express.

The Problem

Consider writing a generic method that adds two numbers:

Example.cs
// This doesn't work — there's no way to express "T has a + operator"
public T Add<T>(T left, T right)
{
    return left + right; // Compile error
}

Before C# 11, the only workarounds were reflection, dynamic dispatch, or writing separate overloads for each numeric type. None were satisfactory.

The Solution

Static abstract interface members let interfaces declare static members that implementing types must provide:

Example.cs
public interface IFactory<TSelf> where TSelf : IFactory<TSelf>
{
    static abstract TSelf Create();
    static abstract TSelf Create(string input);
}

public class Widget : IFactory<Widget>
{
    public string Name { get; init; } = "";

    public static Widget Create() => new() { Name = "Default" };
    public static Widget Create(string input) => new() { Name = input };
}

The key insight is that these members are accessed through the type parameter, not through an instance:

Example.cs
public T CreateDefault<T>() where T : IFactory<T>
{
    return T.Create(); // Calls the static method on the actual type
}

var widget = CreateDefault<Widget>(); // Calls Widget.Create()

Generic Math

The primary motivation for this feature was generic math. .NET 7 introduced a set of numeric interfaces in System.Numerics:

Example.cs
public T Sum<T>(IEnumerable<T> values) where T : INumber<T>
{
    T result = T.Zero; // Static property from INumberBase<T>

    foreach (var value in values)
    {
        result += value; // Operator from IAdditionOperators<T, T, T>
    }

    return result;
}

// Works with any numeric type
var intSum = Sum(new[] { 1, 2, 3, 4, 5 });           // 15
var doubleSum = Sum(new[] { 1.5, 2.5, 3.0 });        // 7.0
var decimalSum = Sum(new[] { 10.0m, 20.0m, 30.0m }); // 60.0

The INumber<T> interface brings together dozens of static abstract members including arithmetic operators, comparison operators, parsing methods, and constants like Zero and One.

Key Numeric Interfaces

The generic math interfaces form a hierarchy:

Example.cs
// Parse any numeric type from a string
public T ParseNumber<T>(string input) where T : IParsable<T>
{
    return T.Parse(input, CultureInfo.InvariantCulture);
}

// Check if a value is within bounds
public bool IsInRange<T>(T value, T min, T max)
    where T : IComparisonOperators<T, T, bool>
{
    return value >= min && value <= max;
}

// Compute average for any numeric type
public T Average<T>(IReadOnlyList<T> values)
    where T : INumber<T>
{
    var sum = values.Aggregate(T.Zero, (acc, val) => acc + val);
    return sum / T.CreateChecked(values.Count);
}

Defining Your Own Interfaces

Static abstract members are not limited to math. They enable any pattern where behaviour belongs to the type rather than an instance:

Example.cs
public interface ISerialiser<TSelf> where TSelf : ISerialiser<TSelf>
{
    static abstract TSelf Deserialise(ReadOnlySpan<byte> data);
    byte[] Serialise();
}

public class Message : ISerialiser<Message>
{
    public required string Content { get; init; }
    public required DateTime Timestamp { get; init; }

    public static Message Deserialise(ReadOnlySpan<byte> data)
    {
        // Deserialisation logic
        return JsonSerializer.Deserialize<Message>(data)!;
    }

    public byte[] Serialise()
    {
        return JsonSerializer.SerializeToUtf8Bytes(this);
    }
}

public T RoundTrip<T>(T original) where T : ISerialiser<T>
{
    var bytes = original.Serialise();
    return T.Deserialise(bytes); // Static call through the type parameter
}

Static Virtual Members

Alongside static abstract members, interfaces can declare static virtual members with default implementations:

Example.cs
public interface IIdentifiable<TSelf> where TSelf : IIdentifiable<TSelf>
{
    static abstract string TypeName { get; }
    static virtual string DisplayPrefix => TSelf.TypeName + ": ";
}

public class Invoice : IIdentifiable<Invoice>
{
    public static string TypeName => "Invoice";
    // DisplayPrefix defaults to "Invoice: "
}

Implementing types can override the virtual member or accept the default.

Operators in Interfaces

Operators are static by nature, so static abstract members finally allow interfaces to require operators:

Example.cs
public interface IVector<TSelf> where TSelf : IVector<TSelf>
{
    static abstract TSelf operator +(TSelf left, TSelf right);
    static abstract TSelf operator *(TSelf vector, double scalar);
    static abstract TSelf Zero { get; }
}

public readonly record struct Vec2(double X, double Y) : IVector<Vec2>
{
    public static Vec2 operator +(Vec2 left, Vec2 right)
        => new(left.X + right.X, left.Y + right.Y);

    public static Vec2 operator *(Vec2 vector, double scalar)
        => new(vector.X * scalar, vector.Y * scalar);

    public static Vec2 Zero => new(0, 0);
}

public TSelf ScaleAndAdd<TSelf>(TSelf a, TSelf b, double factor)
    where TSelf : IVector<TSelf>
{
    return a + b * factor;
}

Limitations and Considerations

Static abstract interface members can only be accessed through a type parameter with the appropriate constraint. You cannot call them through the interface type directly:

Example.cs
// This does not work:
IFactory<Widget>.Create(); // Compile error

// This works — through a constrained type parameter:
T.Create(); // Where T : IFactory<T>

The feature also requires the self-referencing generic pattern (TSelf : IInterface<TSelf>), which can look unusual at first. This pattern ensures that implementing types return their own type rather than the interface type.

When to Use This Feature

Static abstract interface members are the right tool when you need generic code that interacts with type-level concepts: constructors, factory methods, operators, parsing, constants, or any static contract. They replace patterns that previously required reflection or code generation, bringing full type safety and performance to scenarios that were previously out of reach for the generic system.