Static Members in Generic C# Classes

In C#, static members inside generic classes are not shared across all instances of the open generic type; instead, they are unique to each closed generic type. When a generic class is supplied with different type arguments, the Common Language Runtime (CLR) treats each unique combination as a distinct type at runtime. Consequently, each specialized type maintains its own independent set of static fields, properties, methods, and constructors.

How the Runtime Treats Generic Static Members

When you define a generic class like Repository<T>, the CLR does not allocate static memory for the unconstructed type Repository<>. Memory for static members is only allocated once the generic class is instantiated or referenced with a specific type argument, such as Repository<int> or Repository<string>.

Because Repository<int> and Repository<string> are distinct runtime types, their static fields do not share state. Modifying a static variable in Repository<int> has zero effect on the static variable in Repository<string>.

Code Example

Consider the following implementation of a generic counter:

public class ItemTracker<T>
{
    public static int Count = 0;

    public static void Increment()
    {
        Count++;
    }
}

If you modify and access the static fields across different type arguments:

ItemTracker<int>.Increment();
ItemTracker<int>.Increment();

ItemTracker<string>.Increment();

Console.WriteLine(ItemTracker<int>.Count);    // Outputs: 2
Console.WriteLine(ItemTracker<string>.Count); // Outputs: 1
Console.WriteLine(ItemTracker<double>.Count); // Outputs: 0

Each constructed type has its own isolated memory location for Count.

Static Constructors in Generic Classes

Static constructors also run independently for each closed type. A static constructor inside a generic class executes once per closed constructed type, precisely before the first instance of that specific type is created or any of its static members are accessed.

public class Logger<T>
{
    static Logger()
    {
        Console.WriteLine($"Initialized for: {typeof(T).Name}");
    }

    public static void Log() { }
}

// Execution:
Logger<int>.Log();    // Prints: Initialized for: Int32
Logger<int>.Log();    // Does not print (already initialized)
Logger<string>.Log(); // Prints: Initialized for: String

Sharing Static State Across All Type Arguments

If your architecture requires a static member to be genuinely shared across all type arguments of a generic class, you must move the static member into a non-generic base class.

// Non-generic base class
public abstract class EntityBase
{
    public static int TotalEntitiesCreated { get; protected set; }
}

// Generic derived class
public class Entity<T> : EntityBase
{
    public Entity()
    {
        TotalEntitiesCreated++;
    }
}

In this pattern, creating an instance of Entity<int> and an instance of Entity<string> will both increment the single, shared TotalEntitiesCreated static field defined on EntityBase.