Choosing identity and behavior
A class can represent an entity with its own identity, such as a session or connection. Two sessions with identical fields are not necessarily the same session. A data value, such as a limit identified by name and quantity, may have another rule: two instances with matching components can represent the same information.
A record adds generated behavior for data models, including equality. It does not remove the distinction between classes and structs. record without another qualifier declares a record class, which remains a reference type. Assigning that reference to another variable does not create a snapshot. A record struct remains a value type.
First choose the rule the consumer needs to observe. If content comparison matters, define exactly which components belong to that identity. If tracking an object through changes matters, do not confuse current field equality with entity identity. A keyword reduces repetitive code but does not decide the product meaning of the data.
Virtual dispatch and hiding
A variable can be declared as Policy while containing a RegionalPolicy instance. If Policy declares a virtual method and RegionalPolicy overrides it, a call through the Policy variable uses the applicable override for the object. The consumer uses a common contract without manually choosing every implementation.
new on a member has another meaning: it hides an inherited member. Selection of a hidden member depends on the expression’s type at the call. If the base has a nonvirtual Name method and the derived class declares new Name, calling through a base variable still uses the base member. Replacing override with new merely to remove an error can change existing consumers’ behavior.
sealed override prevents later overrides of that member without necessarily sealing the entire class. Even when overriding is permitted, behavior should still satisfy the contract. Do not silently narrow accepted inputs or give success a different meaning. Signature compatibility is a compilation condition; behavioral compatibility requires review.
Interfaces and colliding contracts
An interface lets a consumer request a capability without requiring a concrete class. A consumer receiving IExporter needs the Export contract rather than knowledge of every adapter detail. This supports replacing implementations provided that promises about format, failure and effects remain consistent.
Two interfaces can contain a method with the same signature but different expectations. If ILegacy.Export returns CSV and IModern.Export returns JSON, one public method always returning JSON does not meet both contracts. Explicit implementations, string ILegacy.Export() and string IModern.Export(), associate behavior with the interface used by the caller.
An explicit member is called through its interface. A concrete-typed variable does not automatically gain a public Export method with that implementation. Converting to the interface does not create a second adapter: it changes the accessible contract over the same object. Document each contract’s effects and format so the difference is not hidden only in the method body.
Initialization is not validation
In positional declaration record Allocation(string Region, int Units), the record class’s generated properties are init-only. The object can be initialized, but a later assignment to Region is not permitted in that form. A variant can be created using a with expression while retaining the original.
required demands member initialization in the relevant construction; it does not prove that the value has valid meaning. With required string? Note, assigning null satisfies initialization because the property permits null. Omitting Note from an initializer that must supply it remains an error. An empty string can also be assigned to a string property without violating the required obligation.
Domain rules need their own validation. If Units must be positive or Region must belong to a set, neither init nor required establishes those facts. Keep these checks at a clear boundary and explain what happens on failure. Use modifiers to express the construction contract without presenting them as automatic business validation.
Equality and copying have limits
Two record classes constructed separately with matching string and int components can compare equal with == while remaining distinct objects for ReferenceEquals. Generated equality also distinguishes runtime record types: a base and derived record do not become equal merely because base fields match.
An array member needs special care. In a record containing int[], member equality does not automatically compare every element. Different arrays with matching numbers remain different references. If two records share the same array, that member can compare equal and an array edit can be visible through both.
with performs a shallow copy by default. It copies member references without cloning every reachable object. Consequently, init alone does not guarantee immutable history. To preserve a string array against element edits in another array, separate the containers and control who retains mutable references. For elements that are themselves mutable objects, copying policy must consider those objects too.
Guided example: a replaceable policy
The example exposes a batch admission decision through an interface. IAdmission receives a quantity and returns a Boolean; FixedAdmission accepts 1 through 80. Plan creates a Receipt record containing input data and the decision. It does not enqueue work, reserve capacity or confirm delivery.
With an IAdmission reference to FixedAdmission, Plan(policy, 40) produces an accepted Receipt. With 0 or 81, Accepted is false. Both record components are simple values; there is no shared mutable container. Another Receipt with matching components can compare equal without representing the same business execution.
This distinction matters in real work. An equal local decision is neither an idempotency key nor proof of a single execution. If the system later produces effects, it needs its own operational identity, failure handling and evidence. This exercise isolates interface-based implementation selection and decision representation.
Review the contract observed by the consumer
When reviewing an API, begin with the consumer’s declared type and the object’s actual type. Identify whether the call uses virtual dispatch, hiding or interface implementation. Then check promised format and effects rather than assuming that identical signatures guarantee identical behavior.
For data models, separate four questions: must the member be initialized, can it be reassigned, can the referenced object change, and how is equality compared? required, init, readonly references and record address different parts of this problem. An array inside a record shows why these questions are not equivalent.
Summary: override participates in a virtual contract; new hides; explicit interfaces distinguish contracts; required controls initialization; records add equality and copying with concrete rules. The following questions ask for decisions about these rules and limits, including preserving history and compatibility between consumers.
public interface IAdmission
{
bool Accept(int units);
}
public sealed class FixedAdmission : IAdmission
{
public bool Accept(int units) => units >= 1 && units <= 80;
}
public record Receipt(int Units, bool Accepted);
public static class Planning
{
public static Receipt Plan(IAdmission policy, int units)
=> new Receipt(units, policy.Accept(units));
}Type declarations. Assume policy is non-null. Planning.Plan(new FixedAdmission(), 40) represents an accepted decision; 0 and 81 are rejected without external effects.
Common pitfalls
Replacing override with new; calling an explicit member through the concrete class; treating required as validation; assuming deep array copies in a record.
Related topics: References, nullability and text · Methods, parameters and patterns · Collections, generics and equality
A contract includes behavior, identity and data ownership; keywords express only parts of it.
Reference: C# record types · C# 14 / .NET 10; no preview features