Generic Records and Positional Parameters in C

C# generic records combine the concise syntax and value-based equality of record types with the flexibility of generic type parameters. By using positional parameters, generic records allow developers to define strongly typed, immutable data models with automatically generated properties, primary constructors, and deconstruction logic in a single line of code. This article explains how generic records operate, how positional parameters function with generic types, and how to apply them effectively in modern C# applications.

What Are Generic Records?

Introduced in C# 9, records are reference types (or value types via record struct in C# 10+) designed primarily for immutable data modeling. Unlike standard classes, records implement value-based equality semantics out of the box, meaning two instances are considered equal if their types and property values match.

A generic record introduces one or more type parameters (<T>), allowing the record to operate over various data types without sacrificing type safety or repeating boilerplate code.

public record ApiResponse<T>
{
    public T? Data { get; init; }
    public bool Success { get; init; }
    public string? ErrorMessage { get; init; }
}

How Positional Parameters Work

Positional parameters provide a shorthand syntax to declare properties and a primary constructor directly in the record declaration. When you declare positional parameters on a generic record, the C# compiler automatically generates:

  1. Public init-only properties for each positional parameter with matching names and generic types.
  2. A primary constructor whose parameters match the positional parameters in order and type.
  3. A Deconstruct method allowing easy pattern matching and tuple-like deconstruction.
  4. Value-based equality methods (Equals, GetHashCode, and the == / != operators) tailored to the properties.

Basic Syntax and Usage

When combining generics and positional syntax, place the generic type parameters after the record name and before the positional parameter list:

public record Result<T>(T Data, bool IsSuccess, string Message);

In this single line, Data becomes a public property of type T with an init accessor.

Instantiation and Deconstruction

Working with a generic positional record requires providing the generic type argument (either explicitly or inferred) and supplying the constructor arguments:

// Type inference automatically resolves T as string
var successResult = new Result<string>("Operation completed", true, string.Empty);

// Accessing auto-generated properties
Console.WriteLine(successResult.Data); // Outputs: Operation completed

// Deconstruction works automatically via compiler-generated Deconstruct
var (data, isSuccess, message) = successResult;

Non-Destructive Mutation (with Expression)

Because positional generic records create init-only properties, instances are immutable by default. To alter values, use the with expression to create a new instance with specified property modifications:

var updatedResult = successResult with { Message = "Updated status" };

Using Multiple Type Parameters and Constraints

Generic positional records support multiple type arguments as well as standard generic type constraints. Constraints are placed after the positional parameter list:

public record KeyValueStore<TKey, TValue>(TKey Key, TValue Value)
    where TKey : notnull
    where TValue : class, new()
{
    public bool HasDefaultValue => Value is not null;
}

In this example, TKey is constrained to non-nullable types, while TValue must be a reference type with a parameterless constructor. You can also define additional properties, methods, or secondary constructors inside the body of the record alongside positional parameters.

Generic Positional Record Structs

By default, the record keyword defines a reference type (record class). You can also declare value-type records by combining record struct with generics and positional parameters:

public readonly record struct Measurement<T>(T Value, string Unit);

Using readonly record struct guarantees that the value-type record remains immutable, avoiding unexpected defensive copying while retaining value-equality semantics and positional parameter convenience.