C .NET Design Flaws
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
C# and .NET are well-designed for enterprise development, but they carry design decisions from the early 2000s that cause friction for modern developers. These are not bugs — they are deliberate choices that turned out to be suboptimal as the language and ecosystem evolved. Understanding these flaws helps developers work around them and write better code.
Null Reference Types Were Not in the Original Design
The biggest source of runtime crashes in C# is NullReferenceException. Reference types were nullable by default from C# 1.0, with no compile-time enforcement:
C# 8.0 introduced Nullable Reference Types (NRT) as an opt-in feature, but it is not enforced at runtime — it is only a compiler analysis:
The flaw: NRT is purely a compile-time annotation. It does not prevent null at runtime because existing code and libraries may not have adopted it.
Structs vs Classes — Confusing Semantics
C# has both value types (structs) and reference types (classes), but their behavior differences cause subtle bugs:
The guideline is to make structs immutable, but the language does not enforce this (until readonly struct was added in C# 7.2).
Covariant Arrays Are Unsafe
C# arrays are covariant, meaning string[] can be assigned to object[]. This is type-unsafe:
This was inherited from Java for compatibility. Generic collections (List<T>) do not have this problem because they are invariant. Arrays should have been invariant from the start.
IDisposable and the using Pattern
Resource cleanup through IDisposable is verbose and error-prone. Forgetting to call Dispose() leaks resources:
The flaw: there is no compile-time enforcement that IDisposable objects are disposed. You can new up a SqlConnection and forget the using with zero warnings.
Event Handler Memory Leaks
C# events hold strong references to subscribers, preventing garbage collection:
The fix requires manually unsubscribing (-=), which developers frequently forget. Weak events were never built into the language.
DateTime API Issues
The DateTime type lacks timezone awareness, leading to frequent bugs:
DateTime.Kind (Local, Utc, Unspecified) is a runtime-only property — the type system does not distinguish them.
LINQ Deferred Execution Surprises
LINQ queries use deferred execution, which confuses developers:
The query is not a snapshot — it re-evaluates against the current state of numbers each time it is iterated.
async void Is Dangerous
async void methods cannot be awaited and swallow exceptions:
async void exists only for compatibility with event handler delegates. It should be avoided everywhere else.
Common Pitfalls
- String comparison:
string.Equalsdefaults to ordinal comparison, but==uses ordinal too. For culture-aware comparison, you must explicitly passStringComparison.CurrentCulture— easy to forget and causes bugs in internationalized apps. - Mutable structs: Structs that are mutable behave unexpectedly when boxed, stored in collections, or used as
readonlyfields. Always make structsreadonly structwithinitproperties. - Enum underlying type: Enums in C# are just integers with names. You can cast any integer to an enum type, even invalid values:
(Color)999compiles and runs without error. - Exception hierarchy:
Exceptionis the base class, butSystemExceptionandApplicationExceptionserve no practical purpose. The distinction was a design mistake acknowledged by Microsoft. - Default interface implementations: Added in C# 8.0, these allow interfaces to have method bodies — blurring the line between interfaces and abstract classes and potentially causing diamond inheritance confusion.
Summary
- Nullable reference types were retrofit in C# 8.0 but lack runtime enforcement
- Covariant arrays are type-unsafe and throw runtime exceptions instead of compile-time errors
IDisposablehas no compile-time enforcement — forgettingusingleaks resources- Events hold strong references, causing memory leaks when subscribers are not unsubscribed
DateTimelacks proper timezone support — preferDateTimeOffsetfor new codeasync voidswallows exceptions silently — always useasync Task

