Proposal: Lightweight nominal primitive types (newtype) for stronger ID safety #9996
Replies: 7 comments 12 replies
-
|
See: #410 |
Beta Was this translation helpful? Give feedback.
-
|
This looks identical to an 'explicit role'. Where you would be blocked from conversions unless writing an explicit cast. |
Beta Was this translation helpful? Give feedback.
-
Why is tehre resistance to one, but not resistance to your approach? Both are just named entities that wrap around an underlying type. Why is the role version
If this is only at compile time, then the final metadata representation won't include this, which means anyone consuming this will see this as the underlying type. If you want this to be visible to the consumers, there has to be something in metadata/il indicating that it's this new thing, with new semantics, so that you don't violate this immediately. This has to bake-through to the end code, or else this is just going to be useful within a compilation unit, and not a full program/ecosystem. |
Beta Was this translation helpful? Give feedback.
-
|
Thank you — these are important points. Regarding the similarity to explicit roles: I agree that semantically they are very close. My intent is not to introduce fundamentally new semantics beyond nominal distinction with controlled conversions. The motivation here is largely about ergonomics and adoption. In practice, strongly-typed ID patterns often face resistance due to verbosity and perceived friction. A declaration form that communicates "this is a distinct identity type backed by a primitive" in a minimal and declarative way may improve adoption compared to extension/role-based syntax, even if the underlying semantics are similar. That said, if explicit roles already fully satisfy the nominal distinction + zero-overhead requirement, then it would be useful to understand whether roles are intended to cover this use case directly. Regarding compile-time-only aliasing: you're absolutely right that if the distinction does not survive into metadata, it cannot provide cross-assembly safety. My original wording may have been imprecise. The real requirement is zero runtime cost, not necessarily absence of metadata identity. If preserving nominal identity across assembly boundaries requires distinct metadata representation, that would be acceptable as long as layout and performance characteristics remain equivalent to the underlying primitive. So the core requirement is:
I'm open to clarification on whether roles are the intended long-term solution for this scenario, or whether a more specialized construct for primitive-backed identity types would be considered. |
Beta Was this translation helpful? Give feedback.
-
|
Thank you for the clarification — that helps a lot. If roles are intended to provide a nominal, metadata-preserving, zero-overhead wrapper, then that aligns closely with the problem I’m trying to address. The specific scenario I care about is strongly-typed IDs in domain-driven systems, for example: all backed by In large codebases this pattern dramatically reduces accidental mix-ups, but today it requires either record structs (with overhead and boilerplate) or source generators. If roles are the intended direction, I’d be very interested in understanding:
My goal isn’t to introduce competing syntax, but to ensure this extremely common use case is handled cleanly and ergonomically in the final design. |
Beta Was this translation helpful? Give feedback.
-
|
Specifically for this: public class OrderItem
{
public int Id { get; set; }
public int OrderId { get; set; }
}The better pattern is: public class OrderItem
{
public int Id { get; set; }
public Order Order { get; set; }
}Then ORM maps that as a many-to-one, gives you options to hydrate, etc. public readonly struct EntityRef<TEntity> where TEntity : EntityBase
{
public EntityRef(int id) { this.Id = id; }
public EntityRef(TEntity entity) { this.Id = entity.Id; }
public int Id { get; }
// equality, ToString, etc. boilerplate
}
// elsewhere
void DeleteOrder(EntityRef<Order> orderRef) { ... } |
Beta Was this translation helpful? Give feedback.
-
|
Those are good patterns, and I agree they work well in many ORM-centric designs. However, the scenario I’m targeting is slightly different. Navigation properties (e.g., For example:
In those cases, we often want:
Generic wrappers like
The core motivation here isn’t that the problem is unsolvable today — it clearly is — but that this pattern is common enough that a first-class, low-friction language construct could significantly improve ergonomics while preserving performance. If roles are meant to cover that cleanly, then that would be a very compelling direction. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Lightweight Nominal Primitive Types (NewType) for C#
Champion
Seyed Javad Seyedi
Summary
This proposal introduces a lightweight nominal type feature for
primitive types in C#.
The goal is to allow developers to define distinct, compile-time
enforced identifier types that:
Proposed syntax:
Motivation
Real-World Bug Pattern
In large enterprise systems, especially those following Domain-Driven
Design (DDD), identifier confusion is a recurring issue.
Example:
Bug example:
This category of bug:
After decades of enterprise development experience and extensive code
reviews, this pattern remains common.
Current Workarounds and Their Limitations
record struct Wrapper Pattern
Problems:
Third-Party Libraries
Strongly-typed ID libraries exist but:
Design Goals
Proposed Syntax
Semantics
OrderIdis a distinct nominal type.int.Example:
Explicit conversion (subject to design discussion):
Runtime Representation
Preferred approach:
Pure compile-time aliasing.
The compiler enforces nominal type distinctions but emits the underlying
primitive type in IL.
Advantages:
Example Usage in DDD
Invalid assignment:
Generics Interaction
newtypebehaves like its underlying primitive for generic constraints.If the underlying type satisfies constraints such as
unmanagedornotnull, thenewtypeautomatically satisfies them as well.No new generic constraint syntax is introduced in this proposal.
This keeps the feature minimal and avoids unnecessary complexity in the
type system.
Serialization Behavior
Default behavior:
EF Core Compatibility
This is critical for enterprise adoption.
Performance Requirements
Must guarantee:
Backward Compatibility
Non-Goals
Conclusion
This proposal addresses:
It enables safer domain modeling without sacrificing performance or
simplicity.
Beta Was this translation helpful? Give feedback.
All reactions