The CTS rules for value and reference types, members, inheritance and how language keywords map to CTS types.
The CTS is the type model defined by ECMA-335 that every .NET language compiles to. It defines value types and reference types, the members a type can have (fields, methods, properties, events, nested types), accessibility, single class inheritance with multiple interfaces, and that everything ultimately derives from System.Object. C# int is just an alias for the CTS type System.Int32, which is why a C# library can be used from F# or VB without conversion.
The Common Type System (CTS) is the CLR's specification of what types are and how they behave: value and reference types, classes, interfaces, structs, enums, delegates, members, inheritance, visibility, and how they're represented in metadata. Every .NET language compiles to the same CTS types, which is why a class written in F# or VB.NET can be used, inherited and called from C# with no glue code.
System.ValueType): structs, enums, primitives (int, double, bool); stored inline; copied by value.System.Object (value types can be boxed to it).public, private, family/protected, assembly/internal, etc.), virtual/abstract/sealed, static members, generics with constraints and variance.// F#
type Money(amount: decimal, currency: string) =
member _.Amount = amount
member _.Currency = currency
// C# (referencing the F# assembly)
var m = new Money(10m, "AUD");
Console.WriteLine(m.Amount);
int in C# is the same System.Int32 as Integer in VB.NET.Not every language feature exists in the CTS, and vice versa:
get_X, set_X) plus property metadata.NullableAttribute), not CTS types.dynamic is object + attributes.ValueTuple<...>.modreq/modopt).The CLS (Common Language Specification) is the subset of the CTS that all languages agree to support for interoperable public APIs.
| Value type | Reference type | |
|---|---|---|
| Storage | Inline (stack, inside objects/arrays) | Heap; variable holds reference |
| Assignment | Copies the value | Copies the reference |
| Null | Not null (use Nullable<T>) |
Can be null |
| Inheritance | Sealed, derive from ValueType |
Single inheritance |
| Equality default | Field-wise (ValueType.Equals) |
Reference |
| Boxing | Needed to treat as object/interface |
N/A |
These CTS rules drive performance and correctness decisions (structs vs classes, boxing, equality).
A team shared a domain library written in F# (discriminated unions for pricing rules) with C# services. Because both compile to CTS types, C# consumed the F# types directly; but F# unions appeared as class hierarchies with compiler-generated members (IsVolumeDiscount, Item properties), and F# options as FSharpOption<T> — awkward in C#. We added a thin C#-friendly facade (records and T? returns) at the boundary. Understanding that "cross-language" means "shared CTS types, not shared idioms" made the boundary design obvious.
The interviewer checks whether you understand the runtime type model behind C# syntax.
Green flags: value versus reference types described by semantics, base types for enum, delegate and array, keywords as aliases, reified generics, link to CLS.
Red flags: stack versus heap as the only distinction; thinking int and Int32 differ; unaware that generics are a runtime feature.
Type definitions live in metadata tables: TypeDef (flags for class/interface/sealed/abstract/sequential layout...), Field, MethodDef, Property, Event, InterfaceImpl, GenericParam, NestedClass. The CLR's type loader reads these and builds runtime structures (MethodTable, EEClass). Type flags encode CTS semantics like Sealed, Abstract, Interface, visibility, layout (Auto, Sequential, Explicit).
Value types are sealed classes deriving from System.ValueType (enums from System.Enum) in metadata. The CLR treats them specially: unboxed instances have no object header; boxing creates a heap object with a MethodTable and copies the value. Primitive types (int32, float64) have dedicated IL encodings but map to System.Int32 etc.
Generics are first-class in the CTS (since .NET 2.0): generic type definitions, instantiations, constraints, variance (+T, -T in IL for interfaces/delegates). Reified generics mean List<int> and List<string> are distinct runtime types.
[TypeForwardedTo] redirects references (used heavily in .NET's reference assemblies and netstandard).[TypeIdentifier]) for embedded interop types.The CTS includes unmanaged pointers (int32*), managed pointers (int32& — ref in C#), and function pointers (method ...*), which C# exposes via unsafe code and delegate*.
typeof(int).BaseType and typeof(DayOfWeek).BaseType.TypeDef flags with System.Reflection.Metadata.Console.WriteLine(typeof(int) == typeof(Int32)); // True: int is an alias
Console.WriteLine(typeof(Point).BaseType); // System.ValueType
Console.WriteLine(typeof(Status).BaseType); // System.Enum
Console.WriteLine(typeof(Func<int>).BaseType); // System.MulticastDelegate
Console.WriteLine(typeof(int[]).BaseType); // System.Array
Console.WriteLine(typeof(IDisposable).BaseType is null); // True: interfaces have no base type
// Value semantics versus reference semantics
var p1 = new Point(1, 2);
var p2 = p1; // copy of the value
p2.X = 10;
Console.WriteLine(p1.X); // 1
var o1 = new Box { Value = 1 };
var o2 = o1; // copy of the reference
o2.Value = 10;
Console.WriteLine(o1.Value); // 10
// Boxing: the struct is copied into a heap object
object boxed = p1;
Console.WriteLine(boxed.GetType()); // Point
struct Point(int x, int y) { public int X = x; public int Y = y; }
enum Status { Pending, Shipped }
class Box { public int Value; }
What happens: using the terms interchangeably. Why it is wrong: CLS is a subset of CTS for cross-language public APIs. Do instead: explain both and their relationship.
What happens: expecting nullable reference types or tuple names at run time. Why it is wrong: they're compiler metadata, not CTS types. Do instead: know which features are compile-time only.
What happens: oversimplified CTS explanation. Why it is wrong: value types live inline wherever their container is. Do instead: explain inline storage vs references.
What happens: F# options or unions exposed directly to C# consumers. Why it is wrong: awkward APIs despite shared types. Do instead: design boundary-friendly APIs.
What happens: value types treated as objects in hot paths. Why it is wrong: allocations and copies. Do instead: use generics and avoid boxing.
What happens: expecting same-named types in different assemblies to be equal. Why it is wrong: identity includes the assembly. Do instead: share types via a common assembly or type forwarding.
What happens: assuming Java-style erasure.
Why it is wrong: .NET generics are distinct runtime types.
Do instead: use typeof(T) and generic statics knowingly.
What happens: assuming properties are storage in the type system. Why it is wrong: they're methods; changing field ↔ property breaks binary compatibility. Do instead: expose properties from the start in public APIs.
The CTS matters most when you cross a boundary: another language, reflection, serialisation or interop.
| Boundary | CTS fact that matters |
|---|---|
| F# or VB consuming a C# library | Only CTS (and ideally CLS) constructs travel |
| Reflection and serialisers | Properties and events are metadata over methods |
| Interfaces on structs | Calling through the interface boxes unless a generic constraint is used |
| Generic performance | Reified generics give value types specialised code |
A senior engineer uses this to predict allocations and API compatibility instead of memorising definitions.
The CLS is the subset of the CTS that every .NET language is expected to support, so a CLS-compliant public API can be used from C#, F#, VB and others. Typical rules: no unsigned types such as uint in public signatures, no public identifiers that differ only by case, no pointer types in public members. Adding [assembly: CLSCompliant(true)] makes the C# compiler warn (CS3001, CS3002, CS3005 and others) when a public member breaks the rules. Only the public surface matters; internal code can use anything.
Boxing wraps a value type in a new heap object so it can be used as object or an interface; unboxing checks the object's type and copies the value back, and it must be the exact type or you get InvalidCastException. Every box is a heap allocation, so in hot paths it creates GC pressure. Hidden boxing comes from non-generic APIs like ArrayList and params object[], passing structs as interfaces, calling GetType() or non-overridden Equals/GetHashCode on structs, and enumerating a List<T> through IEnumerable<T>. I find it with BenchmarkDotNet's MemoryDiagnoser or an allocation profiler and fix it with generics, constraints and IEquatable<T>.
Generics are usually zero-cost, but how depends on T. For value types the JIT generates separate, fully specialised code, so there is no boxing and typeof(T) == typeof(int) folds to a constant. For reference types all instantiations share one canonical body, which looks up type-specific data such as statics and typeof(T) through a runtime dictionary. new T() and generic virtual methods take slower paths. The cost of specialisation is code size and JIT time, which matters for Native AOT, where every instantiation must be known at build time.
How to tell it (STAR)
On [the pricing service], we wanted F# for rules but C# for the API. I showed the team in ILSpy that F# records are ordinary CTS classes, so we referenced the library directly and removed [a planned mapping layer of about N classes]. We kept only CLS-friendly types on the boundary.
Last updated 30 Oct 2026.