Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.syntblaze.com/llms.txt

Use this file to discover all available pages before exploring further.

A generic record in C# is a data-centric type that combines the value-based equality and non-destructive mutation semantics of records with the type safety of generics. It allows the definition of a record blueprint where one or more property types are deferred until instantiation via type parameters.

Syntax and Declaration

Generic records support both positional and nominal declaration styles. Type parameters are declared immediately following the record identifier.
// Positional syntax (generates a primary constructor and init-only properties)
public record Wrapper<T>(T Content, DateTime Timestamp);

// Nominal syntax (explicit property definition)
public record Container<T>
{
    public required T Item { get; init; }
}

// Generic record struct (value type semantics)
public readonly record struct Point<T>(T X, T Y);

Type Constraints

Standard generic type constraints (where clauses) apply to generic records. In positional syntax, the constraint is placed after the primary constructor parameters.
public record ConstrainedRecord<T>(T Value) where T : struct, IEquatable<T>;

Compiler-Synthesized Members

When the C# compiler generates a generic record, it synthesizes several members adapted to the type parameter T:
  1. Value Equality (Equals and ==): The compiler implements IEquatable<TRecord>. For generic properties, it utilizes EqualityComparer<T>.Default.Equals() to ensure correct equality checks regardless of whether T resolves to a reference type or a value type.
  2. Hashing (GetHashCode): The synthesized hash code incorporates the generic properties using EqualityComparer<T>.Default.GetHashCode().
  3. Deconstruction: For positional generic records, the compiler generates a Deconstruct method matching the generic signature of the primary constructor.
  4. Formatting (PrintMembers): The compiler generates a PrintMembers method that calls ToString() on the generic properties to support built-in record string formatting.

Non-Destructive Mutation (with Expressions)

Generic records fully support with expressions. The compiler synthesizes a hidden copy constructor that accepts an instance of the generic record. The resulting type of a with expression retains the exact constructed type of the original instance.
var original = new Wrapper<int>(42, DateTime.UtcNow);

// The type of 'mutated' is strictly Wrapper<int>
var mutated = original with { Content = 100 }; 

Inheritance Rules

Generic records follow strict inheritance rules specific to the record type system:
  • A record class can only inherit from another record class (or object).
  • A generic record can inherit from a non-generic record, provided it supplies the required base constructor arguments.
  • A generic record can inherit from another generic record, passing its type parameters up the inheritance chain.
public record BaseRecord<T>(T Id);

// Inheriting and passing the type parameter
public record DerivedRecord<T, U>(T Id, U Payload) : BaseRecord<T>(Id);

// Inheriting and closing the generic type
public record ClosedDerivedRecord(string Id, int Count) : BaseRecord<string>(Id);

Covariance and Contravariance

Because generic records are classes or structs (not interfaces or delegates), their type parameters cannot be declared as covariant (out) or contravariant (in). A Wrapper<string> cannot be implicitly cast to a Wrapper<object>, even though string derives from object. Type parameters in generic records are strictly invariant.
Master C# with Deep Grasping Methodology!Learn More