Chapter 1, titled The CLR’s Execution Model,
introduces the fundamental architecture, mechanics, and terminology of the .NET
Framework. Here is a summary of its key concepts:
- Compiling
Source Code into Managed Modules:
- The
Common Language Runtime (CLR) is an execution engine usable by
multiple programming languages, providing shared core features like
memory management, security, and exception handling.
- Compilers
translate source code into a managed module, which is a standard
Windows portable executable (PE32 or PE32+) file that requires the CLR to
execute.
- A
managed module contains a PE header, a CLR header, Intermediate
Language (IL) code, and Metadata.
- Metadata
thoroughly describes the types and members defined and referenced in the
code, ensuring the code and metadata are never out of sync.
- Combining
Managed Modules into Assemblies:
- The
CLR does not work directly with modules; it works with assemblies,
which are logical groupings of one or more managed modules or resource
files.
- An
assembly acts as the smallest unit of reuse, security, and versioning.
- Assemblies
contain a manifest—a set of metadata tables that describes all the
files, exported types, and resources that make up the assembly.
- Loading
the Common Language Runtime:
- Developers
can specify a platform target (such as x86, x64, ARM, or anycpu) which
determines how the application will be loaded and run on various Windows
operating systems.
- When
an application starts, Windows loads the appropriate version of the CLR
into the process's address space (via MSCorEE.dll) before invoking the
application's Main entry point.
- Executing
Your Assembly’s Code:
- To
execute a method, the CLR uses its Just-In-Time (JIT) compiler to
dynamically convert the CPU-independent IL code into native CPU
instructions.
- A
performance hit is only incurred the first time a method is called;
subsequent calls execute the already-compiled native code at full speed.
- During
compilation, the CLR performs verification to ensure the IL code
is safe and performs only "type-safe" operations, preventing
data corruption and security breaches.
- The
Native Code Generator Tool (NGen.exe):
- NGen.exe
can compile an assembly's IL into native code at install time to improve
application startup time and reduce its memory working set.
- However,
NGen'd files have significant drawbacks: they offer no intellectual
property protection, can easily fall out of sync with the runtime
environment (reverting to JIT compilation), and often produce less
optimized code than the JIT compiler.
- The
Framework Class Library (FCL):
- The
FCL is a vast collection of DLL assemblies containing thousands of types
for building various applications (e.g., Web services, Web Forms, console
apps, Windows services).
- Related
types are logically grouped into namespaces (such as System, System.IO,
and System.Threading) to present a consistent programming paradigm to
developers.
- The
Common Type System (CTS):
- The
CTS is a formal specification that dictates how types and their members
(fields, methods, properties, events) are defined and behave within the
CLR.
- It
defines access control rules (such as public, private, and assembly) and
mandates that all types must ultimately inherit from a predefined root
type: System.Object.
- The
Common Language Specification (CLS):
- The
CLS defines a minimum subset of CTS features that compilers must support
to ensure seamless interoperability between different programming
languages.
- If
developers want a type to be accessible from other languages, its
externally visible members must adhere strictly to CLS rules.
- Interoperability
with Unmanaged Code:
- The
CLR supports interaction with existing unmanaged code, allowing managed
code to call unmanaged native functions via P/Invoke (Platform
Invoke).
- It
also allows unmanaged code to use managed types by exposing them as COM
components.
Chapter 2
- DLL Hell: Applications heavily rely on dynamic-link libraries (DLLs) from various vendors. Because shared DLLs were traditionally dumped into a single system directory (like
System32), installing a new application could overwrite an existing DLL with a newer (or older) incompatible version, immediately breaking other applications that relied on it. - Installation Complexities: Installing an application used to mean scattering state all over a user's hard drive. Files were copied to various directories, registry settings were updated, and shortcuts were created. This meant you couldn't just easily back up or copy an application from one machine to another; you had to run complex installation and uninstallation programs, often leaving you with a nasty feeling that hidden files were left lurking on your machine,.
- Security Fears: Users were right to be terrified of installing new software. Applications and downloaded Web components (like ActiveX) could secretly install themselves and perform malicious operations, such as deleting files or sending unauthorized emails.
Program class and a Main method that calls System.Console.WriteLine("Hi").csc.exe) using the command line. For example: csc.exe /out:Program.exe /t:exe /r:MSCorLib.dll Program.cs- /out: Dictates the name of the output file (
Program.exe). - /t (target): Tells the compiler what kind of Portable Executable (PE) file to create. You can create a console user interface (
/t:exe), a graphical user interface (/t:winexe), or a Windows Store app (/t:appcontainerexe). - /r (reference): Tells the compiler where to look to resolve external types. Because
System.Consoleisn't defined in our code, we must tell the compiler to referenceMSCorLib.dll,. (Note:MSCorLib.dllis usually referenced by default, but you can explicitly block it using the/nostdlibswitch).
@ sign, like this: csc.exe @MyProject.rsp CodeFile1.cs.CSC.rsp located in the same directory as the csc.exe compiler itself. This global file references all the standard Microsoft-published assemblies (like System.Data.dll, System.Xml.dll, etc.), meaning you don't have to explicitly reference them in your daily development. If you don't use a type from those assemblies, the compiler ignores them, so there is no performance penalty or file bloat. If there are conflicting settings, your local response files or explicit command-line switches will override the global CSC.rsp settings.ModuleDef: Identifies the module with its file name and a unique GUID.TypeDef: Contains an entry for every type (class, struct, interface, etc.) defined in the module, including its name, base type, and flags.MethodDef,FieldDef,PropertyDef,EventDef: These contain entries for every method, field, property, and event defined in the module,.
AssemblyRef: Contains entries for external assemblies your module needs, including their name, version, culture, and public key token.ModuleRefandTypeRef: Point to types implemented in different modules or assemblies.MemberRef: Contains entries for the specific external fields and methods your code calls.
ILDasm.exe). By running ILDasm and viewing the metadata statistics, you'll quickly see that in small projects, the metadata and headers take up the vast majority of the file size, while the actual IL code might just be a few bytes,.AssemblyDef, FileDef, ManifestResourceDef, and ExportedTypesDef) that act as a directory describing all the files, versions, and exported types that make up the complete assembly,-.- Incremental Downloading: You can put frequently used types in one file and rarely used types in another. If a user never accesses the rare types, that specific file never needs to be downloaded, saving bandwidth and disk space.
- Resource Partitioning: You can package raw data files (like text files, Excel spreadsheets, or images) as standalone files that belong to the logical assembly.
- Mixed-Language Development: You can compile C# code into one module, Visual Basic code into another, and combine them both into a single logical assembly. To the consumer, it just looks like one seamless component.
- You use the C# compiler's
/t:moduleswitch to create raw modules (which get a.netmoduleextension and have no manifest). - You then link them together using the C# compiler's
/addmoduleswitch, or you can use the standalone Assembly Linker utility (AL.exe).AL.exeis highly flexible, allowing you to link modules built by different compilers or to embed/link raw resource files using the/embedresourceor/linkresourceswitches,.
AssemblyInfo.cs file in your project's Properties folder, providing a handy GUI dialog to edit fields like AssemblyCompany, AssemblyCopyright, and AssemblyDescription.- AssemblyFileVersion: This is strictly informational. It is stored in the Win32 resource and viewed in Windows Explorer to track specific daily builds. The CLR completely ignores it.
- AssemblyInformationalVersion: Also informational and ignored by the CLR. It represents the version of the overarching product (e.g., your assembly might be version 1.0, but it ships as part of Product Suite version 2.0).
- AssemblyVersion: This is the critical one. It is stored in the
AssemblyDefmanifest metadata table. The CLR uses this specific number when resolving dependencies and binding to strongly named assemblies. Ironically, Windows Explorer does not display this attribute, which can sometimes complicate troubleshooting.
- Assemblies containing your core logic and code should be culture-neutral (no culture assigned).
- Culture-specific translations and resources (like translated UI strings or localized images) should be compiled into entirely separate, code-free assemblies known as satellite assemblies.
AL.exe with the /culture switch, and you must deploy them into specific subdirectories named exactly after the culture tag (e.g., C:\MyApp\en-US\). When your application runs, the System.Resources.ResourceManager automatically hunts down the correct satellite assembly based on the user's OS settings..appx files, but they also maintain this strict isolation by destroying the directory entirely upon uninstallation)..config extension (e.g., Program.exe.config),.AuxFiles folder? They can use the .config file to instruct the CLR to look there.<probing privatePath="AuxFiles" /> element, the administrator can define semicolon-delimited paths relative to the base directory. When the CLR searches for an assembly (e.g., AsmName.dll), it executes a strict probing algorithm:- It checks the base directory for
AsmName.dll. - It checks the base directory for a subdirectory matching the assembly name:
AsmName\AsmName.dll. - It checks the defined private paths:
AuxFiles\AsmName.dll. - It checks subdirectories within the private paths:
AuxFiles\AsmName\AsmName.dll.
.dll, the CLR repeats the exact same search pattern looking for an .exe extension. If it still comes up empty, a FileNotFoundException is thrown.App.config files, there is also a global Machine.config file located in the CLR installation directory. This file governs machine-wide policies for all applications using that specific version of the CLR, though modifying it directly is highly discouraged as it breaks the clean isolation of application-specific deployments.Chapter 3
Mastering .NET Shared
Assemblies: A Deep Dive into Chapter 3
If you have ever
grappled with the infamous "DLL Hell," you know the pain of shared
components breaking existing applications. While private deployment (keeping
assemblies in the application's base directory) gives us a great deal of
control over versioning and behavior, modern software development often
requires us to share code securely and reliably across multiple applications,.
In this comprehensive
guide, we will elaborate on every single section of Chapter 3 from Jeffrey
Richter's CLR via C#, completely demystifying how the Microsoft .NET
Framework handles shared assemblies, strong naming, versioning, and
administrative control. Grab a comfortable seat; we are going deep into the
internals of the Common Language Runtime (CLR)!
--------------------------------------------------------------------------------
Two Kinds of Assemblies,
Two Kinds of Deployment
To solve the deployment
and sharing problems of the past, the CLR categorizes assemblies into two
distinct types: weakly named assemblies and strongly named assemblies.
Structurally, these two
types are completely identical. They use the same PE32(+) file format, contain
the same CLR header, carry the same metadata and manifest tables, and are
compiled using the exact same tools (like the C# compiler and AL.exe).
The critical difference
lies in cryptographic identity: a strongly named assembly is signed with a
publisher's public/private key pair. This signature allows the assembly to be
uniquely identified, secured, versioned, and deployed anywhere.
Because of this
distinction, they are deployed differently,:
- Privately Deployed Assemblies: These are placed in the application’s base
directory or a subdirectory. Both weakly named and strongly named
assemblies can be deployed privately.
- Globally Deployed Assemblies: These are placed in a well-known system
location to be shared across multiple applications. Only strongly
named assemblies can be globally deployed.
--------------------------------------------------------------------------------
Giving an Assembly a
Strong Name
Imagine two different
companies both create an assembly called MyTypes.dll. If these were just dumped
into a shared system directory, the last one installed would overwrite the
first, instantly breaking the applications that relied on the older version.
To prevent this, the CLR
must uniquely identify an assembly. A strongly named assembly is uniquely
identified by four attributes:
- File Name (without the extension)
- Version Number
- Culture Identity
- Public Key (often condensed into a Public Key Token)
Because full public keys
are incredibly long numbers, Microsoft uses a Public Key Token—an 8-byte
hash of the public key—to conserve storage space in metadata,.
To give an assembly a
strong name, a company must first generate a public/private key pair using the
Strong Name utility (SN.exe) that ships with the .NET Framework SDK. Once the
key is generated, the compiler takes over.
When building the PE
file, the compiler hashes the file's entire contents (excluding the space
reserved for the signature itself, the strong name data, and the PE header
checksum). This hash is then signed with the publisher's private key,
generating an RSA digital signature that is embedded directly into a reserved
section of the PE file. Finally, the publisher's public key is embedded into
the AssemblyDef manifest metadata table.
Through this
cryptography, there is absolutely no way two companies can produce a
conflicting assembly with the same name unless they specifically share their
private keys with one another.
--------------------------------------------------------------------------------
The Global Assembly
Cache (GAC)
If an assembly is meant
to be shared across multiple applications on a single machine, it must be
placed in a well-known directory where the CLR can automatically find it. This
location is called the Global Assembly Cache (GAC), typically located at
%SystemRoot%\Microsoft.NET\Assembly,.
You should never
manually copy files into the GAC because it possesses a highly specific,
undocumented internal directory structure,. Instead, you must use tools that
understand this structure.
- For Development: Developers use GACUtil.exe. Passing the /i
switch installs an assembly, and the /u switch uninstalls it,.
- For Production: It is highly recommended to use GACUtil.exe
with the /r switch to integrate the assembly with the Windows
install/uninstall engine, safely tying the assembly to the applications
that require it.
Because weakly named
assemblies lack a unique identity, attempting to install one into the GAC will
fail and return an error stating: "Attempt to install an assembly without
a strong name".
--------------------------------------------------------------------------------
Building an Assembly
That References a Strongly Named Assembly
When you compile an
application, you must tell the compiler which external assemblies your code
references using the /reference (or /r) switch.
A fascinating
architectural quirk of the .NET Framework is that compilers do not look
inside the GAC to resolve references at compile time. The GAC's complex
structure makes it difficult to parse, and developers would otherwise have to
specify obnoxiously long paths.
To solve this, Microsoft
actually installs two copies of the Framework assemblies:
- Compiler/CLR Directory: One set contains only metadata (no IL code)
and is architecture-agnostic. This is used strictly by the compiler at
build time to resolve types.
- The GAC: The second set contains full metadata and IL code, heavily
optimized for specific CPU architectures (x86, x64, ARM). The CLR loads
these from the GAC at runtime.
When the compiler
resolves a reference, it embeds an entry into the referencing assembly's AssemblyRef
metadata table, storing the referenced assembly's name, version, culture, and
public key token.
--------------------------------------------------------------------------------
Strongly Named
Assemblies Are Tamper-Resistant
Strong naming isn't just
about identity; it is also about security. Signing an assembly ensures its bits
have not been maliciously altered or corrupted.
When an assembly is
installed into the GAC, the system hashes the manifest file's contents and
compares it to the embedded RSA digital signature (unsigning it with the public
key). If the hashes don't perfectly match, the assembly has been tampered with and
will fail to install into the GAC.
At runtime, when an
application binds to an assembly, the CLR locates it using the properties
stored in the AssemblyRef table.
- For GAC Assemblies: Because the GAC verifies the signature
heavily at installation time, the CLR skips the tamper check at load time
(for fully trusted AppDomains) to boost performance.
- For Privately Deployed Strong Assemblies: The CLR must verify the signature every
single time the file is loaded, incurring a slight performance hit.
--------------------------------------------------------------------------------
Delayed Signing
In large organizations,
the company's private key is treated like gold—locked in a hardware device
inside a highly secure vault. If developers need to build and test strongly
named assemblies daily, how can they do it without access to the private key?
The solution is Delayed
Signing.
- Developers extract only the public
key into a file (using sn.exe -p).
- They compile the assembly using the /keyfile
and /delaysign compiler switches.
- The compiler leaves blank space in the PE
file for the future RSA signature and embeds the public key in the
manifest, but it does not hash the file or sign it.
- Because the file lacks a valid signature,
the CLR will refuse to load it. Developers must temporarily turn off
verification on their local machines using SN.exe -Vr,.
When the software is
fully tested and ready to ship, it is sent to the secure vault where the actual
private key is applied using SN.exe -Ra to fully sign and hash the final build.
Delayed signing is also mandatory if you plan to run post-build tools, like an
obfuscator, which would otherwise invalidate the hash of a fully signed
assembly.
--------------------------------------------------------------------------------
Privately Deploying
Strongly Named Assemblies
Just because an assembly
has a strong name does not mean it belongs in the GAC,.
Deploying to the GAC
requires administrative privileges and completely breaks the beauty of
"simple copy" (XCOPY) deployment. The GAC is not intended to be the
new C:\Windows\System32 dumping ground.
If an assembly is
tightly coupled to a specific application and isn't meant to be shared
system-wide, it should be privately deployed in the application's directory.
You can even deploy a strongly named assembly to an arbitrary network or local
directory and use the XML configuration file's <codeBase> element to
point the CLR to that specific URL,. The CLR will automatically download it to
the user's download cache and execute it.
--------------------------------------------------------------------------------
How the Runtime Resolves
Type References
What exactly happens
when your code calls a method like System.Console.WriteLine?
When the JIT compiler
compiles the Intermediate Language (IL), it sees a metadata token (e.g., 0A000003)
pointing to a MemberRef entry. The CLR follows this to a TypeRef entry, and
finally to an AssemblyRef entry, determining exactly which assembly holds the
required type,.
The CLR resolves the
type in one of three places,:
- Same file: Early bound at compile time; loaded directly.
- Different file, same assembly: The CLR checks the ModuleRef table, finds
the file in the manifest's directory, verifies the hash, and loads it.
- Different assembly: The CLR extracts the AssemblyRef info,
checks the GAC (if it's strongly named), then probes the app's base
directories, and loads the manifest file,.
A massive caveat: Microsoft hard-coded a feature called unification
for .NET Framework assemblies (like MSCorLib.dll). Regardless of what version
is recorded in your AssemblyRef table, references to core framework assemblies
will always silently bind to the exact version that matches the currently
running CLR,.
Furthermore, when the
CLR searches the GAC, it factors in CPU architecture. It will search for a
version of the assembly specifically optimized for the current process (e.g.,
x64) before falling back to a CPU-agnostic (MSIL) version,.
--------------------------------------------------------------------------------
Advanced Administrative
Control (Configuration)
Sometimes, after an
application ships, an administrator needs to change how the application binds
to its dependencies. This is done via XML configuration files (App.config or Machine.config).
A configuration file
allows profound control using specific XML elements,,:
- <probing privatePath="..."/>:
Instructs the CLR to search specific subdirectories for weakly named
assemblies.
- <dependentAssembly>: Wraps binding
rules for a specific assembly.
- <bindingRedirect
oldVersion="..." newVersion="..."/>: Instructs the
CLR to forcefully load a newer (or older) version of an assembly than the
one it was originally compiled against.
- <codeBase href="..."/>:
Instructs the CLR to download the assembly from a specific URL or file
path.
- <publisherPolicy
apply="no"/>: Tells the CLR to ignore publisher-issued
redirects (discussed next).
The CLR reads the
application's config file, checks for Publisher Policy, and finally checks the
machine-wide Machine.config to determine exactly which version to load and
where to find it.
--------------------------------------------------------------------------------
Publisher Policy Control
Imagine you publish an
assembly used by dozens of applications. You discover a critical bug, fix it,
and increment the version number. Because of strong naming, existing
applications will stubbornly look for the old, buggy version. Asking every
single customer to manually edit their application's XML config file to point
to the new version is a nightmare,.
Enter Publisher
Policy.
As the publisher, you
can create a new XML configuration file containing a <bindingRedirect>
that maps the old version to your new version,. You then compile this XML file
into a specialized assembly using AL.exe.
The resulting file must
be named using a very specific format: Policy.<major>.<minor>.<AssemblyName>.dll
(e.g., Policy.1.0.SomeClassLibrary.dll). You sign this policy assembly with the
exact same public/private key pair as the original assembly, proving to
the CLR that you are the authentic publisher.
You then distribute the
new component assembly and the Publisher Policy assembly together, and install
the policy assembly into the GAC. Now, whenever an application requests the old
buggy version, the CLR intercepts the request, reads your policy from the GAC,
and seamlessly redirects the application to the new, fixed version!
If the new version
somehow introduces a worse bug, the local machine administrator always has the
final say. They can simply add <publisherPolicy apply="no"/> to
the application's configuration file, completely ignoring the publisher's
redirect and forcing the application to revert to the old assembly,.
Chapter 4
System.Object type. Because of this rule, writing a class definition like class Employee { } is implicitly identical to explicitly writing class Employee : System.Object { }.System.Object, the runtime guarantees that every single object possesses a minimum, standard set of behaviors. Specifically, System.Object provides four public instance methods available to any type:- Equals: This method returns
trueif two objects have the same value. - GetHashCode: This returns a hash code for the object's value, which is particularly important if the object is going to be used as a key in a hash table collection like a
Dictionary. - ToString: By default, this returns the full name of the type, but developers frequently override it to return a string representation of the object's current state (which is also used automatically by the Visual Studio debugger).
- GetType: This nonvirtual method returns a
Typeobject identifying the exact type of the object, which is heavily used for reflection. Because it is nonvirtual, a type can never override it to spoof its identity, preserving the type safety of the runtime.
System.Object provides two protected methods. MemberwiseClone is a nonvirtual method that creates a shallow copy of the object, while Finalize is a virtual method called by the garbage collector before an object's memory is reclaimed.new operator. When you write a line of code like Employee e = new Employee();, the new operator performs a highly orchestrated sequence of events:- It calculates the total number of bytes required by all instance fields defined in the type and all of its base types. It then adds the bytes required for two internal overhead members—the type object pointer and the sync block index—used by the CLR to manage the object.
- It allocates this required memory from the managed heap and guarantees that all of these bytes are set to zero.
- It initializes the object's type object pointer and sync block index.
- Finally, it calls the type's instance constructor, which cascades up the inheritance hierarchy until
System.Object's parameterless constructor is called.
new operator returns a reference (or pointer) to the newly created object. Unlike C++, there is no complementary delete operator; memory is freed automatically by the CLR's garbage collector when the object is no longer being used.GetType method), making it impossible to trick the runtime into treating an object as something it is not.System.InvalidCastException.is operator. The is operator checks if an object is compatible with a specified type, returning a simple true or false Boolean without ever throwing an exception.using directive. This tells the compiler to automatically try prepending the specified namespaces to type names it doesn't immediately recognize.Microsoft.Widget and Wintellect.Widget), your compiler will throw an ambiguous reference error. You can resolve this by either typing the fully qualified name in your code or by using a special form of the using directive to create an alias, like using WintellectWidget = Wintellect.Widget;. For extreme edge cases—such as two companies both creating an ABC namespace containing a BuyProduct type—C# offers an extern alias feature to programmatically distinguish between the assemblies themselves.System.IO.FileStream is in MSCorLib.dll, while System.IO.FileSystemWatcher is in System.dll). Conversely, a single assembly can house types from entirely different namespaces.- Static Methods: When calling a static method, the JIT compiler locates the specific Type Object that defines the method, finds the corresponding entry in its method table, and executes the JIT-compiled native code.
- Nonvirtual Instance Methods: The JIT compiler locates the Type Object corresponding to the declared type of the variable making the call. If the type doesn't define the method, it walks up the class hierarchy toward
System.Objectuntil it finds it. It then invokes the method. - Virtual Instance Methods: This is where polymorphism shines. The JIT compiler generates code that inspects the actual object on the heap at runtime. It follows the object's internal type object pointer to find its true Type Object, locates the method in that specific method table, and executes it.
System.Type type. The type object pointers for all other Type Objects (like Employee or String) refer to this System.Type type object. And what about the System.Type object itself? Its type object pointer simply points back to itself, completing the CLR's type system loop.GetType() on any object simply returns the address stored in its type object pointer—giving you the true, un-spoofable identity of the objectint keyword in C# maps directly to System.Int32, float maps to System.Single, and string maps to System.String,.Int32 and String) rather than the C# language-specific primitives (like int and string) in your code. Why? Because it completely removes ambiguity across different languages. For example, in C#, a long maps to a 64-bit System.Int64, but in C++/CLI, a long is treated as a 32-bit Int32. Using the explicit FCL type names ensures that any developer reading your code knows exactly what memory footprint the variable consumes, and it perfectly aligns with FCL methods that include the type name (such as BinaryReader.ReadInt32).Int32 to an Int64 without an explicit cast. The compiler knows this is a "widening" operation and is completely safe, so it implicitly performs the conversion. Conversely, casting an Int32 to a Byte is a "narrowing" operation that risks data loss, so the compiler requires you to perform an explicit cast.checked and unchecked operators and statements,.- If you wrap an operation in an
uncheckedblock, the compiler truncates the result and allows the overflow to occur silently. - If you wrap an operation in a
checkedblock, the CLR will throw aSystem.OverflowExceptionif the result exceeds the data type's bounds.
/checked+ switch during debug builds so you can catch overflows early, and using /checked- for release builds to maximize execution speed. However, if your application can afford the slight performance hit, leaving checking on in release builds can prevent your application from running with corrupted data or exposing security holes.new operator, the following things happen:- Memory is allocated from the managed heap.
- Additional overhead members (a type object pointer and a sync block index) are added to the object.
- The variable holding the object only contains a 32-bit or 64-bit pointer (the memory address) to the actual object bits on the heap.
- The object is now tracked by the CLR's Garbage Collector (GC).
System.ValueType, which derives from System.Object. They are also implicitly sealed, meaning they cannot be used as base classes for other types.- It acts like a primitive type and is immutable (meaning its state never changes after construction).
- It doesn't need to inherit from any other type, and nothing will inherit from it.
- Its memory footprint is small (typically 16 bytes or less) so that copying it doesn't hurt performance.
LayoutKind.Auto for classes (allowing the CLR to optimize memory layout) and LayoutKind.Sequential for value types (to preserve field order for interoperating with unmanaged code).Int32 allocated on the stack), but you want to pass it to a method that expects a reference type (like System.Object)? The CLR accomplishes this via a mechanism called Boxing.- It allocates memory on the managed heap (calculating the size of the value type's fields plus the reference type overhead).
- It copies the raw field bytes from the stack-based value type into the newly allocated heap memory.
- It returns the address of this new heap object. The value type is now effectively a reference type.
Console.WriteLine statement that concatenates an integer, a string, and a boxed integer. Because String.Concat expects Object parameters, the CLR must secretly box the integers, allocating new memory on the heap purely to satisfy the method signature,,. You can entirely avoid this specific trap by explicitly calling .ToString() on your value types before concatenating them, which prevents the boxing operation.IComparable) forces a boxing operation because interface variables must always contain a reference to a heap object,.readonly); it prevents you from ever falling into this confusing state-mutation trap.System.Object base class provides a virtual Equals method.Object.Equals implements identity equality—it merely checks if two references point to the exact same memory address on the managed heap. However, System.ValueType overrides Equals to provide value equality, returning true if the two objects' fields contain identical data.ValueType's default Equals implementation uses reflection to iterate over the fields, making it notoriously slow. If you design your own value type, you should always override Equals (and GetHashCode) to provide a fast, reflection-free implementation,.Equals, you must ensure it adheres to four strict rules:- Reflexive:
x.Equals(x)is true. - Symmetric:
x.Equals(y)returns the same asy.Equals(x). - Transitive: If
xequalsy, andyequalsz, thenxequalsz. - Consistent: Repeated calls return the same result provided the objects haven't mutated.
System.IEquatable<T> interface. Finally, because Equals can be overridden to mean "value equality," if you ever need to strictly test for pointer identity (do these two variables point to the exact same heap memory?), you should use the static Object.ReferenceEquals method.dynamic Primitive Typedynamic primitive type,.dynamic is literally just System.Object,. However, to the C# compiler, it represents a completely different set of rules. When you invoke a member (a method, property, or operator) on a dynamic variable, the compiler does not attempt to resolve it or enforce type safety at compile time. Instead, it generates special "payload code".Microsoft.CSharp.dll assembly) to examine the actual, underlying type of the object and dispatch the operation dynamically,. If the runtime type supports the operation, it executes seamlessly. If it doesn't, a RuntimeBinderException is thrown during execution.dynamic with var.varis simply syntactical shorthand. The compiler infers the exact, static type at compile time based on the right side of the assignment.dynamicdisables compile-time type checking entirely, evaluating the expression exclusively at runtime,.
dynamic bypasses compile-time checks, you completely lose Visual Studio IntelliSense for those variables. You also cannot write extension methods directly for dynamic. However, it is an absolute lifesaver for COM interoperability. When importing COM objects, the old VARIANT types are seamlessly projected as dynamic, allowing you to access properties and methods naturally without littering your codebase with hideous casting syntax.- Constants: Symbols representing never-changing data values. Logically, they are always static.
- Fields: Data values representing the state of the type (if static) or the state of an object (if non-static/instance). Richter strongly advises keeping fields private to prevent external code from corrupting state.
- Instance Constructors: Special methods used to initialize the instance fields of a newly created object to a safe, initial state.
- Type Constructors: Special methods used to initialize a type's static fields.
- Methods: Functions that change or query the state of a type or an object.
- Operator Overloads: Methods defining how an object should behave when operators (like
+or-) are applied to it. Since not all languages support operator overloading, these are not part of the Common Language Specification (CLS). - Conversion Operators: Methods defining how to implicitly or explicitly cast an object from one type to another. These are also not CLS-compliant.
- Properties: Smart fields that offer a simplified syntax for getting or setting state while protecting the underlying data. They can be parameterless or parameterful (often called indexers in C#).
- Events: A mechanism that allows a type to send notifications to registered methods when a specific action occurs (typically involving a delegate field).
- Nested Types: Types defined entirely within another type, usually used to break complex implementations into smaller building blocks.
- public: The type is visible to all code in the defining assembly and to all code in any other assembly.
- internal: The type is visible only to code within its own defining assembly. If you don't explicitly specify visibility, C# defaults to
internal.
AssemblyA, and Team B needs to use those utilities in AssemblyB. Team A wants to keep their utilities hidden from the rest of the world to prevent misuse, so making them public is out of the question.[InternalsVisibleTo] attribute to Team A's assembly, Team A can explicitly declare Team B's assembly as a "friend." This allows Team B to access Team A's internal types as if they were public. This is also incredibly useful for unit testing, allowing a separate test assembly to invoke internal methods. When compiling a friend assembly, you must use the C# compiler's /out:<file> switch to help the compiler determine the output file name early, which significantly improves compilation performance.- Private (private): Accessible only by methods within the defining type or its nested types.
- Family (protected): Accessible only by methods in the defining type, nested types, or derived types (regardless of which assembly they live in).
- Family and Assembly (Not supported in C#): Accessible by derived types, but only if they reside in the same assembly.
- Assembly (internal): Accessible by any method in the defining assembly.
- Family or Assembly (protected internal): Accessible by derived types anywhere, OR by any method in the defining assembly.
- Public (public): Accessible to all methods in any assembly.
public method inside an internal class cannot be called by outside assemblies.System.Console or System.Math. In C#, you define these using the static keyword applied to a class. (You cannot apply static to a struct because the CLR always allows value types to be instantiated).static class, the C# compiler enforces several strict rules:- The class must derive directly from
System.Object. - The class cannot implement any interfaces.
- The class may only contain static members.
- The class cannot be used as a field, method parameter, or local variable type.
abstract and sealed in the metadata, and it refuses to emit an instance constructor (.ctor) for it.partial keyword in C# tells the compiler that the source code for a single class, struct, or interface might be spread across multiple files. It is vital to note that partial types are entirely a compiler feature; the CLR knows nothing about them. The compiler simply stitches the parts together at compile time into a single, unified type.- Source Control: Multiple developers can work on different parts of the same class simultaneously without stepping on each other's toes or dealing with messy source-control merges.
- Logical Grouping: You can dedicate a single file to a specific feature of a complex type, making the code easier to read and allowing you to comment out a whole feature simply by removing the file from the build.
- Code Generators and Designers: When using tools like Visual Studio's Windows Forms designers, the tool can spit its generated code into one file, while you write your custom business logic in another. This prevents you from accidentally breaking the designer's code.
- call: Used to call static methods, instance methods, and virtual methods statically. It invokes the method defined by the exact type specified, assuming the variable is not null.
- callvirt: Used to call virtual instance methods. It examines the actual runtime type of the object and invokes the overriding method defined by that specific type. Interestingly, C# often uses
callvirteven for non-virtual instance methods simply becausecallvirtperforms a free null-check, throwing aNullReferenceExceptionif the variable is null.
- Default to sealed: Make classes
sealedby default unless you explicitly intend them to be base classes. Sealed classes prevent rogue derived classes from corrupting your state. They also improve performance because the JIT compiler can optimize virtual method calls into non-virtual calls (since it knows no derived class can override them). - Default to internal: Keep classes hidden inside your assembly unless they absolutely must be public.
- Keep Fields private: Never expose data fields publicly, protected, or internally. Exposing state is the fastest way to lose predictability and introduce security holes.
- Keep Methods Non-Virtual: Avoid
virtualmethods if possible. Virtual methods are slower, cannot be inlined, and relinquish behavioral control to whoever derives from your class. If you provide convenience overload methods, make only the most complex one virtual and keep the rest non-virtual.
CompanyA ships a Phone class with a Dial method. CompanyB derives a BetterPhone class from it and adds their own custom EstablishConnection method. Later, CompanyA updates the Phone class and adds its own virtual EstablishConnection method.CompanyB recompiles against the new Phone class, the C# compiler detects the name collision and issues a warning. CompanyB now has two choices:- Use the new keyword: By adding
newtoBetterPhone'sEstablishConnectionmethod,CompanyBtells the compiler, "My method has absolutely nothing to do with the base class's method." The CLR will treat them as completely separate methods, maintaining the original behavior of theBetterPhoneclass. - Use the override keyword: If
CompanyBdecides that their method should polymorphically override the new base method, they removenewand addoverride. Now, when the basePhoneclass callsEstablishConnection, it will polymorphically route toBetterPhone's implementation.
CompanyB's code, likely breaking it. C#'s strict requirement for new or override is a brilliant defense against the fragility of component versioning.public sealed class SomeLibraryType {
public const Int32 MaxEntriesInList = 50;
}
public sealed class Program {
public static void Main() {
Console.WriteLine("Max entries supported in list: " + SomeLibraryType.MaxEntriesInList);
}
}
MaxEntriesInList is a constant literal with a value of 50. Instead of generating code that looks up this value from your library at runtime, the compiler extracts the value and embeds the Int32 value of 50 right inside the application’s Intermediate Language (IL) code.50 is too small, so you update your library code:public const Int32 MaxEntriesInList = 1000;
1000. It won't.50 hardcoded into its compiled IL, it is completely unaffected by the new DLL. For the application to pick up the new value of 1000, you would have to completely recompile the application assembly against the new library.- Static (static): The field is part of the type’s state, rather than being tied to a specific object instance. The dynamic memory required to hold a static field's data is allocated inside the type object itself. This type object is created when the type is loaded into an AppDomain (typically the first time a method referencing the type is JIT-compiled).
- Instance (default, no keyword): The field is associated with a specific instance of the type. The memory to hold an instance field is allocated dynamically on the managed heap when an instance of the type is constructed.
- InitOnly (readonly): The field can only be written to by code contained within a constructor method. We will dive deeper into this below.
- Volatile (volatile): The field is not subject to certain thread-unsafe optimizations normally performed by the compiler, the CLR, or the hardware. (Note: Only specific primitive types and reference types can be marked volatile).
static readonlyMaxEntriesInList issue from earlier, we simply change the constant to a static read-only field:public sealed class SomeLibraryType {
// The static is required to associate the field with the type.
public static readonly Int32 MaxEntriesInList = 50;
}
MaxEntriesInList out of the dynamic memory allocated for the type.1000 and ships a new DLL, the application will automatically pick up the new value the next time it runs without needing to be recompiled!readonly fields are strictly protected: compilers and CLR verification ensure that they can only be written to within a constructor. (The only exception to this rule is that reflection can be used to bypass it and modify a read-only field).public sealed class SomeType {
// Static read-only field
public static readonly Random s_random = new Random();
// Instance read-only field
public readonly String Pathname = "Untitled";
public SomeType(String pathname) {
// We can overwrite a read-only field here because we are in a constructor
this.Pathname = pathname;
}
}
- Use Constants (const) only for values that are universally true and will absolutely never change (like Pi, or the number of hours in a day).
- Use Static Read-Only Fields (static readonly) for values that act like constants but might be subject to change in future versions of your component. This ensures your consumers will always see the latest value at runtime without recompiling their entire application
.ctor.new operator to instantiate an object, the runtime first allocates the required memory for the object's data fields. Next, it initializes the object's overhead fields—specifically the type object pointer and the sync block index. Crucially, before the constructor method is even executed, the CLR guarantees that the newly allocated memory is zeroed out, meaning any fields you do not explicitly set will automatically have a value of 0 or null. Finally, the type's instance constructor is invoked to establish the initial state of the object.virtual, new, override, sealed, or abstract to an instance constructor.abstract, the generated default constructor will be given protected accessibility rather than public. If you declare a static class (which is technically abstract and sealed), the compiler will not emit a default constructor at all.base(...), the C# compiler will automatically insert a call to the base class's default constructor. This chain cascades all the way up the inheritance hierarchy until System.Object's parameterless constructor is invoked, which simply returns doing nothing, as System.Object contains no instance data fields to initialize.this(...) keyword.Rectangle class that contains two Point value type fields (m_topLeft and m_bottomRight). When you construct a new Rectangle, the CLR allocates memory for the Rectangle (which includes the memory for the two Point structs inline). For performance reasons, the CLR does not automatically attempt to call a constructor for every value type field embedded within the reference type. Instead, the fields of the value types are simply initialized to 0 or null. A value type's instance constructor is only executed if you explicitly invoke it.public Point() { ... }, the compiler will throw error CS0568: "Structs cannot contain explicit parameterless constructors".this keyword represents an instance of the value type itself, and you can actually assign a completely new instance to it (e.g., this = new Point();), which effectively zeroes out all the fields at once.static keyword. You must never apply an access modifier (like public or private) to a type constructor; C# automatically makes them private to prevent developer-written code from invoking them. The CLR is the only entity that should ever call a type constructor.- Value Types: While C# allows you to define a type constructor for a struct, you should avoid doing so. The CLR does not always call a value type's static constructor (for instance, when allocating an array of that value type), which can lead to uninitialized state.
- Circular References: If
ClassA's type constructor referencesClassB, andClassB's type constructor referencesClassA, the CLR cannot guarantee the order of execution, which may lead to unpredictable behavior. You should never write code that relies on type constructors executing in a specific sequence. - Exceptions: If a type constructor throws an unhandled exception, the CLR considers the type permanently unusable in that AppDomain. Any subsequent attempt to access the type will throw a
System.TypeInitializationException.
+, -, ==, etc.) on custom types, making code much more intuitive.+ operator for a Complex math class, the compiler emits a method named op_Addition. This method is flagged with the specialname metadata flag, which signals to compilers that this method represents a special operator. Later, when a compiler encounters a + symbol between two Complex objects, it searches for the specialname method called op_Addition with compatible parameters and emits the IL to invoke it.+ operator (which creates op_Addition), you should also explicitly define a public method named Add that internally calls the operator overload. This allows a developer using a language without operator support to simply call Complex.Add(c1, c2) to achieve the exact same result. The FCL's System.Decimal type is a perfect role model for this design pattern.Rational fraction class to an Int32 or a Single.public and static method. You also must specify whether the conversion is implicit or explicit.- implicit: Used when the conversion is guaranteed to succeed and no data precision will be lost (e.g., converting an
Int32to aRational). - explicit: Used when the conversion might throw an exception or result in a loss of precision (e.g., converting a
Rationalto anInt32, which might truncate decimal places).
op_Implicit and op_Explicit in the resulting metadata. When a developer writes code that casts an object, the C# compiler detects the cast and generates IL that calls the appropriate conversion operator method behind the scenes.(Int32)myRational). The C# compiler will never invoke your custom conversion operators when evaluating the is or as keywords.StringBuilder class, such as an IndexOf method. Historically, you would write a static helper class with a static method, requiring callers to write StringBuilderExtensions.IndexOf(sb, '!'). This breaks the fluent, object-oriented readability of code (reading left-to-right) and forces programmers to memorize the existence of your obscure helper class.this keyword before the first parameter.public static class StringBuilderExtensions {
public static Int32 IndexOf(this StringBuilder sb, Char value) { ... }
}
sb.IndexOf('X'). When the compiler parses this, it first looks for an actual instance method named IndexOf on the StringBuilder class. If it doesn't find one, it searches imported static classes for an extension method where the first parameter matches the calling type, and it generates the IL to call your static method.this keyword, the C# compiler secretly applies the System.Runtime.CompilerServices.ExtensionAttribute to the method, the enclosing class, and the entire assembly. This metadata flag allows the compiler to rapidly filter and locate extension methods at compile time.- You must import the namespace containing the extension class using a
usingdirective for the compiler to "see" the extensions. - Because extension methods are ultimately just static method calls, invoking an extension method on a
nullobject reference will not automatically throw aNullReferenceExceptionat the call site; the exception will only occur if your extension method's internal logic attempts to dereference the null parameter. - You can extend interface types (like
IEnumerable<T>), which is the foundational mechanism behind Microsoft's Language Integrated Query (LINQ) technology. - Versioning Hazard: If Microsoft updates the base class in the future to include an instance method with the exact same signature as your extension method, the C# compiler will prioritize the true instance method upon recompilation, potentially altering your application's behavior. Use extension methods judiciously.
virtual methods that you would override in a derived class. However, this required the class to be unsealed, wasted system resources allocating virtual methods that did nothing by default, and forced the evaluation of arguments even if the hook was never used.partial class containing a defining partial method declaration—a method marked with the partial keyword that has no body.partial class and provide the implementing partial method declaration—the same method signature, also marked partial, but with your custom code body. The C# compiler stitches them together at compile time.- They must be declared within a
partial classorpartial struct. - They must always return
void. - They cannot have
outparameters (because if the method is erased, the variable would remain uninitialized). - They are implicitly
private, though C# forbids you from actually typing theprivatekeyword in the declaration. - You cannot create a delegate that points to a partial method, as the method might literally not exist at runtime.
- Placement: Parameters with default values must appear at the end of the parameter list, after all required parameters. The only exception is a
paramsarray, which must be the absolute last parameter, but cannot have a default value itself. - Compile-Time Constants: Default values must be constants known at compile time. This limits defaults to primitive types, enums,
nullfor reference types, or a default value type initialized with zeroes (e.g.,default(DateTime)). - No Ref/Out: You cannot assign default values to parameters marked with the
reforoutkeywords. - Ordering: When invoking a method, you can mix positional and named arguments, but named arguments must always appear at the end of the invocation list.
"A", any external application that calls this method without specifying the argument will have "A" hardcoded into its Intermediate Language (IL). If you later update your library to change the default value to "B" and ship the new DLL, the external application will still pass "A" until that application is completely recompiled against the new library.null or 0) as the default. Inside the method body, you can check for the sentinel and apply the true default logic. This allows you to safely change the default behavior in future library updates without breaking compiled clients.System.Runtime.InteropServices.OptionalAttribute and System.Runtime.InteropServices.DefaultParameterValueAttribute to the parameter's metadata, allowing other languages to discover and utilize these defaults.var)var keyword for local variables. Instead of explicitly writing out complex type names on both sides of an assignment (e.g., Dictionary<String, Single> x = new Dictionary<String, Single>();), you can simply write var x = new Dictionary<String, Single>();.- Cleaner Code and Refactoring:
vardrastically reduces typing and makes refactoring easier. If you change a method's return type, you don't have to manually update the type declarations for every variable that receives that return value; the compiler figures it out automatically. - Broad Usage: You can use
varinsideforeach,using, andforstatements. It is also completely mandatory when working with anonymous types because the compiler generates a type name you literally cannot know or type. - Strict Scoping: You cannot use
varto declare a method's parameters or a class's fields. The C# team mandated this to prevent anonymous types from leaking outside their defining method and to ensure that API contracts (field types and parameters) are strictly and explicitly stated.
var vs. dynamicvar with dynamic.varis pure syntactical sugar; the variable is strongly and statically typed at compile time.dynamic, on the other hand, completely disables compile-time type checking for a variable, deferring all validation to the CLR at runtime. You cannot cast an expression tovar, but you can cast an expression todynamic.
ref and out)out and ref keywords. When you use these keywords, you are passing the memory address of the variable rather than a copy of the variable itself.out vs. ref- out: Used when a method needs to return multiple values. A variable passed as
outdoes not need to be initialized before the method call. However, the method receiving theoutparameter is strictly required to assign a value to it before returning. - ref: Used when a method needs to read and potentially modify an existing value. A variable passed as
refmust be initialized by the caller before being passed into the method.
out or ref with large value types can yield significant performance benefits because passing a memory pointer avoids the overhead of copying large structs across method boundaries.out or ref keyword at the call site (e.g., GetVal(out x)), even though the compiler already knows the method's signature requires it. The C# language designers mandated this explicit syntax so the programmer reading the code can easily see that the method intends to mutate the state of the variable being passed.ref/out). However, you cannot overload a method where the only difference is that one takes ref and the other takes out, because they compile down to the exact same metadata signature.params)String.Concat. C# accomplishes this using the params keyword.params Int32[] values, you can call the method by passing a comma-separated list of integers (e.g., Add(1, 2, 3, 4, 5)) instead of explicitly writing the clunky code to allocate and populate an array (Add(new Int32[] { 1, 2, 3, 4, 5 })).params keyword instructs the compiler to apply the System.ParamArrayAttribute to the parameter. When a C# compiler encounters a method call, it checks if a standard overload exists. If not, it checks if a method exists with the ParamArrayAttribute. If it finds a match, the compiler automatically generates the invisible code to construct the array on the heap, populate it with your arguments, and pass the array to the method.params Object[].params provides beautiful syntax, there is a hidden performance cost. Because the compiler must silently allocate an array on the managed heap for every call, it creates memory pressure and triggers garbage collections.System.String.Concat) explicitly define multiple, non-params overloads for the most common argument counts (e.g., taking one, two, three, or four discrete parameters). The params overload acts as a catch-all for the rare, less-common scenarios, ensuring the performance hit is only incurred when absolutely necessary.IEnumerable<T> rather than List<T>. This massively expands the utility of your method, allowing callers to pass in arrays, lists, or custom collections without being forced to convert their data into a highly specific format just to use your API.List<String>, but only want the user to treat it as a list, return IList<String> instead of List<String>. This allows you to safely change your internal implementation in the future (perhaps returning an array instead) without breaking any consumer code. You do not want to return the absolute weakest type (like IEnumerable<String>) if the caller realistically needs list-like capabilities, so IList<String> provides the perfect balance of encapsulation and utility.const keyword for method parameters or instance methods. In unmanaged C++, marking a method or parameter as const supposedly guarantees that the object's state cannot be modified.const modifier or grab the direct memory address of the object to bypass the restriction and mutate the state anyway. Because const essentially "lied to programmers, making them believe that their constant objects/arguments couldn’t be written to even though they could," the designers of the CLR deliberately excluded it.params allocations to dodging versioning traps with optional parameters—you ensure that your .NET libraries are resilient, performant, and a joy for other developers to consume.Employee object might have Name and Age fields. However, exposing these fields directly to the public is a cardinal sin in object-oriented design because it breaks data encapsulation. If an Age field is completely public, malicious or buggy code could easily set an employee's age to -5, corrupting the object's state.private and provide public accessor methods (like GetName and SetAge) to retrieve or modify the data. These methods can act as gatekeepers, performing sanity checks to ensure the state remains uncorrupted (e.g., throwing an ArgumentOutOfRangeException if the age is less than 0). They also allow you to add thread-safety, execute side effects, or lazily evaluate values.e.Age = 48;).get accessor, a set accessor, or both. The set accessor automatically contains a hidden parameter named value, which represents the incoming data.Name, the compiler emits a get_Name method and a set_Name method into the managed assembly. It also emits a special property definition entry into the assembly's metadata, which draws an association between the abstract concept of the "property" and its underlying methods. The CLR itself only cares about the methods at runtime; the metadata merely exists for compilers and reflection tools.public String Name { get; set; }, the C# compiler takes over the heavy lifting. It automatically declares a hidden, private backing field and implements the get and set accessor methods to read and write to this hidden field.- No Binary Compatibility on Changes: Because the compiler generates the name of the hidden backing field, this name can change if you ever recompile your code. This will break any runtime serialization that relies on the field's name, so you should avoid AIPs in types marked with the
[Serializable]attribute. - Debugging Limitations: You cannot place a breakpoint on an AIP's
getorsetaccessor, making it impossible to detect when your application is reading or writing to the property during debugging. - All-or-Nothing: An AIP must have both a
getand asetaccessor. If you explicitly implement one accessor, you must explicitly implement the other, completely losing the AIP feature for that specific property.
- Exceptions: Accessing a field never throws an exception, but a property is a method, and methods can throw exceptions.
- Reference Passing: You cannot pass a property as an
outorrefparameter to a method, whereas you can do this with a field. - Execution Time: A field access is instantaneous. A property might take a long time to execute, especially if it performs thread synchronization or crosses remote boundaries.
- Consistency: A field always returns the same value if it hasn't been modified. A property, however, might return a different value on every call (a classic example being
DateTime.Now). - Side Effects: Querying a field has no side effects, but a property method might alter internal state or return a copy of an object rather than the actual internal object itself.
GetXxx and SetXxx) instead of a property to avoid developer confusion.get accessor method every single time you hit a breakpoint.get accessor reaches across the network, queries a database, or modifies internal state (like incrementing a counter), hitting a breakpoint will trigger these operations, potentially altering the state of your application just because you looked at it in the debugger!Employee e = new Employee() { Name = "Jeff", Age = 45 };..ToString() on the result.IEnumerable or IEnumerable<T>, C# considers it a collection. You can initialize it using a comma-separated list of items in braces. The compiler handles this by automatically emitting calls to the collection's Add method for every item you specify. If the collection requires multiple arguments for its Add method (like a Dictionary), you can pass them using nested braces.var keyword and object initializer syntax without specifying a class name (e.g., var o1 = new { Name = "Jeff", Year = 1964 };), you instruct the C# compiler to automatically generate an immutable tuple type behind the scenes.Object's Equals, GetHashCode, and ToString methods, ensuring that your anonymous types can be safely used as keys in hash tables and easily examined in the debugger.Tuple classes that vary by the number of generic parameters (arity), allowing you to group anywhere from one to eight (or more) items together. Like anonymous types, Tuples are immutable and automatically implement methods like Equals, GetHashCode, and ToString.Item1, Item2, Item3, etc.. These names lack semantic meaning, which can severely reduce code readability and maintainability. It is up to the producer and consumer of the Tuple to mutually understand what Item1 actually represents, usually requiring extensive code comments. If you need a more dynamic but readable property grouping, you might consider using the System.Dynamic.ExpandoObject class combined with C#'s dynamic keyword.get accessor, the CLR fully supports properties that accept parameters. C# refers to these as Indexers, while Visual Basic calls them Default Properties.this[...]), essentially allowing you to overload the [] operator for your custom types. This is incredibly useful for associative arrays, dictionaries, or custom bit arrays.this keyword instead of a specific name, the compiler automatically assigns the default name Item to the generated methods, resulting in get_Item and set_Item being emitted into the metadata.Item might not be semantic. You can easily change this compiler-generated name by applying the [IndexerName("YourName")] attribute to your indexer definition. For example, the System.String class renames its indexer to Chars to make it explicitly clear that you are retrieving characters from the string. Note that C# only allows indexers to be applied to instances of objects; it does not support static indexers, even though the CLR itself allows them.[] for indexers, it cannot distinguish between multiple parameterful properties that have the exact same parameters but different names. In fact, C# will throw a compiler error if you try to define two indexers with the same parameter signature.System.Reflection.DefaultMemberAttribute to the class, specifying the name of the indexer (which defaults to "Item" unless overridden by the IndexerName attribute). If a C# developer consumes a class written in another language that has multiple parameterful properties, C# will only be able to access the specific property designated by the DefaultMemberAttribute.get and set accessors.public, but explicitly mark its set accessor as protected. The strict rule here is that the property itself must be declared with the least-restrictive accessibility (e.g., public), and you apply the more restrictive accessibility (e.g., protected or private) to the specific accessor method you wish to hide.public T MyProp<T> { get; }).Button, the button raises a Click event, and any registered objects receive a notification so they can perform a corresponding action.MailManager receives incoming emails and raises a NewMail event to notify registered Fax and Pager objects.System.EventArgs, and the class name should end with EventArgs. For our scenario, we define a NewMailEventArgs class containing read-only properties for the sender, recipient, and subject of the message. If your event does not require any additional data to be passed, you can simply use the static EventArgs.Empty field rather than allocating a new object.event keyword. public event EventHandler<NewMailEventArgs> NewMail; This line specifies that listeners must supply a callback method matching the generic EventHandler<TEventArgs> delegate prototype. This prototype mandates that event handlers return void and accept two parameters: an Object (representing the sender) and the EventArgs object. Why is the sender typed as a generic Object instead of the specific MailManager type? This design provides flexibility and supports inheritance. If a derived class like SmtpMailManager raises the event, the method prototype remains consistent; listeners won't be forced to change their method signatures. Furthermore, the return type must be void because an event might notify dozens of registered callbacks, making it impossible to handle multiple return values seamlessly.protected virtual method responsible for actually raising the event. This allows derived classes to override the method and control how or if the event is raised. Thread safety is critical here. Historically, developers checked if the event delegate was null and then invoked it, but a race condition could occur if another thread removed the last listener right after the null check, resulting in a NullReferenceException. The most technically correct way to prevent this race condition in modern .NET is to use Volatile.Read to safely copy the delegate to a temporary variable before invoking it:protected virtual void OnNewMail(NewMailEventArgs e) {
EventHandler<NewMailEventArgs> temp = Volatile.Read(ref NewMail);
if (temp != null) temp(this, e);
}
EventArgs object, and triggers the event by calling the method defined in Step 3. In our example, a SimulateNewMail method receives the email details, creates the NewMailEventArgs object, and passes it to OnNewMail.public event EventHandler<NewMailEventArgs> NewMail;, you are actually invoking a tremendous amount of syntactic sugar. The C# compiler translates this single line into three distinct constructs in your assembly:- A Private Delegate Field: The compiler generates a private field (initialized to
null) that maintains the linked list of registered delegates. It is madeprivatespecifically to prevent outside code from maliciously or accidentally wiping out the entire list of registered listeners. - A Public add_ Method: The compiler generates an
add_NewMailmethod that allows objects to register their interest. Internally, this method callsSystem.Delegate.Combineto append the new delegate to the chain. - A Public remove_ Method: The compiler generates a
remove_NewMailmethod that allows objects to unregister. This internally callsSystem.Delegate.Remove. If code attempts to remove a delegate that was never added, the method simply does nothing and returns without throwing an exception.
Interlocked.CompareExchange. The compiler also emits an event definition entry into the managed module's metadata to draw an association between the concept of the event and these underlying accessor methods, which tools and reflection APIs can utilize.Fax object) simply defines a callback method whose signature matches the event's delegate prototype.mm.NewMail += FaxMsg;. When the compiler sees this operator, it translates it into code that instantiates the delegate and passes it to the add_NewMail method generated in the previous section. When the MailManager later raises the event, the FaxMsg method executes, extracting the necessary data from the NewMailEventArgs object.IDisposable interface, you should always ensure that its Dispose method unregisters from all events to prevent memory leaks.System.Windows.Forms.Control class, for instance, defines approximately 70 events. If the compiler implicitly generated a delegate field for all 70 events, every single button or textbox in your UI would waste an enormous amount of memory for events that are rarely used.EventSet—to hold all event delegates for the object.System.Windows.EventHandlersStore.add and remove accessors yourself, directly manipulating your central collection:public event EventHandler<FooEventArgs> Foo {
add { m_eventSet.Add(s_fooEventKey, value); }
remove { m_eventSet.Remove(s_fooEventKey, value); }
}
+= and -= operators to subscribe and unsubscribe, remaining blissfully unaware that the events are being managed explicitly behind the scenes to drastically reduce memory consumption.- Source Code Protection: Unlike C++ templates, which require the algorithm's source code to be available to the consumer, a generic algorithm in .NET is compiled into Intermediate Language (IL) and encapsulated in an assembly.
- Compile-Time Type Safety: When you use a generic algorithm and specify a type, the compiler guarantees that only compatible objects are used. Attempting to pass an incompatible type (like a
Stringinto a list ofDateTimeobjects) results in a compile-time error, preventing runtime crashes. - Cleaner Code: Because the compiler enforces type safety, there is no need to write messy, explicit casts in your code to extract objects from a generic collection.
- Vastly Improved Performance: Prior to generics, placing value types (like
Int32) into non-generic collections (likeArrayList) required the CLR to "box" the value into an object on the heap, and then "unbox" it when retrieved. This created massive memory pressure and triggered frequent garbage collections. Generics eliminate boxing entirely for value types, yielding a massive performance boost.
System.Collections.ArrayList). Instead, developers should rely on the generic collections found in the System.Collections.Generic and System.Collections.ObjectModel namespaces, or the thread-safe collections in System.Collections.Concurrent.System.Array base class itself leverages generics heavily. It provides dozens of highly optimized, static generic methods—such as Sort<T>, BinarySearch<T>, ConvertAll, Find, and ForEach—allowing you to execute complex algorithms directly on standard arrays with complete type safety.Dictionary<TKey, TValue>) is considered an open type because its type parameters are unspecified. The CLR strictly prohibits the creation of an instance of an open type.Dictionary<String, Guid>), it becomes a closed type. Only closed types can be instantiated. If you leave even one parameter unspecified, it remains an open type and attempts to create an instance via reflection (e.g., Activator.CreateInstance) will throw an ArgumentException.List<T> derives from System.Object, a closed type like List<String> also derives directly from System.Object. You cannot cast List<String> to List<Object> because they are entirely distinct types to the CLR.< and >) can get messy, developers sometimes try to clean up their code by creating a derived class: internal sealed class DateTimeList : List<DateTime> { }.DateTimeList is not considered the same type as a List<DateTime>, meaning you cannot pass your custom list into methods expecting a standard generic list. If you want cleaner syntax, use a using directive alias at the top of your file instead: using DateTimeList = System.Collections.Generic.List<System.DateTime>;.Int32), it generates native CPU instructions optimized exclusively for that exact value type. If you use a List<Int32> and a List<Double>, the JIT compiler produces two completely different sets of native code. This is known as code explosion, and it can increase your application's working set and memory footprint.List<String> and List<Stream> share the exact same compiled native code, heavily mitigating code explosion.- Type Safety and Cleaner Code: A generic interface (like
IEnumerator<T>) allows its properties and methods to use the specified typeT, preventing the need to cast fromSystem.Object. - No Boxing for Value Types: If a value type implements a non-generic interface (like
IComparable), passing it requires boxing. A generic interface (likeIComparable<T>) takes the value type directly, preserving performance. - Multiple Implementations: A single class can implement the same generic interface multiple times with different type arguments. For example, a
Numberclass can implement bothIComparable<Int32>andIComparable<String>, providing distinct sorting logic for different comparison types.
WaitCallback, TimerCallback, etc.). With the advent of generics, this bloat is no longer necessary. Microsoft now provides 17 generic Action delegates (for methods returning void) and 17 generic Func delegates (for methods returning a value), capable of accepting up to 16 parameters. Developers should use these predefined delegates wherever possible instead of defining custom delegate types.- Invariant: The default behavior. The generic type parameter cannot be changed.
- Contravariant (in keyword): The generic type argument can change from a base class to a derived class. Contravariant types can only appear in input positions (e.g., method parameters).
- Covariant (out keyword): The generic type argument can change from a derived class to a base class. Covariant types can only appear in output positions (e.g., method return types).
Func delegate is defined as Func<in T, out TResult>. Because T is contravariant, a Func<Object, ArgumentException> can be safely cast to a Func<String, Exception>. You can pass a String where an Object is expected (contravariance), and you can return an ArgumentException where a generic Exception is expected (covariance).Display(123), the compiler infers that the type argument T is Int32 and automatically calls Display<Int32>(123) for you.T is just a System.Object—you can do very little with a generic type by default. You can assign it, call ToString(), or call Equals(), but you cannot invoke a method like CompareTo() because the compiler cannot guarantee that the unknown type T will actually have a CompareTo() method.where keyword. Constraints limit the kinds of types that can be passed into the generic algorithm, which in turn proves to the compiler that certain methods or behaviors will definitely be available.- Primary Constraints: You can specify zero or one primary constraint.
- A specific, unsealed reference type (e.g.,
where T : Stream). This promises the compiler thatTwill be the specified class or a class derived from it. class: Promises the compiler thatTwill be any reference type (class, interface, delegate, or array). This allows you to safely set a variable of typeTtonull.struct: Promises the compiler thatTwill be a value type (excludingNullable<T>). Because all value types implicitly have a parameterless constructor, this allows you to safely usenew T().
- A specific, unsealed reference type (e.g.,
- Secondary Constraints: You can specify zero or more secondary constraints, which are interface types. This promises the compiler that the type passed in will implement the specified interface(s), allowing you to safely call the interface's methods.
- Constructor Constraints: By specifying
new(), you promise the compiler that the type will have a public, parameterless constructor. This allows your generic code to dynamically instantiate new objects of typeT.
T to "types that support the + or - operators." Primitive types (like Int32 and Double) do not implement operator interfaces; the compiler just knows how to compile them directly. Consequently, you cannot easily write a generic mathematical algorithm (like a generic calculator) that applies mathematical operators to an unknown generic type T.System.Object. By deriving from Object, a class automatically inherits the signatures and implementations of four instance methods: ToString, Equals, GetHashCode, and GetType. This guarantees a baseline of functionality for every type in the system.- It cannot define instance constructors.
- It cannot define any instance fields.
- While the CLR technically allows interfaces to contain static methods, fields, and constructors, the Common Language Specification (CLS) expressly forbids them. Consequently, the C# compiler will prevent you from adding static members to an interface.
IDisposable, IEnumerable) to make them easily identifiable in source code.ICollection<T> inherits IEnumerable<T> and IEnumerable. This means any class implementing ICollection<T> is forced to provide implementations for the methods of all three interfaces.public.virtual implementation, derived classes can easily override it. If you don't make it virtual, the method is implicitly sealed. A derived class cannot override a sealed interface method, but it can re-inherit the same interface and provide its own brand-new implementation using the new keyword.ICloneable cloneable = new String(...), you are restricting the operations you can perform on that object.cloneable variable points to a String object in the managed heap, the compiler will only let you call the methods defined by the ICloneable interface (i.e., Clone()). You cannot call ToUpper() or Length using this variable. However, because the CLR knows that all types ultimately derive from System.Object, you are perfectly allowed to call Object methods—like GetType() or ToString()—through an interface variable,.System.Object, every new method introduced by the type, and every method required by the interfaces the type implements,.- Implicit Implementation: When you implement an interface normally (e.g.,
public void Dispose()), the C# compiler performs a neat trick. It maps both the class's ownDisposemethod entry and theIDisposable.Disposeentry in the method table to the exact same block of implementation code,. - Explicit Interface Method Implementation (EIMI): Sometimes, you want to implement an interface method, but you don't want it to be part of the class's public API. You can achieve this by prefixing the method name with the interface name:
void IDisposable.Dispose() { ... }.
public or private. Under the hood, the C# compiler automatically marks the method as private. This prevents anyone from calling myObject.Dispose() directly. The only way to invoke an EIMI is to cast the object to the interface type first: ((IDisposable)myObject).Dispose(). Furthermore, EIMIs cannot be marked as virtual, meaning they cannot be overridden by derived classes.- Compile-Time Type Safety: A generic interface like
IEnumerator<T>ensures you are working with the correct data types without relying on error-prone runtime casts fromSystem.Object,. - No Boxing for Value Types: Before generics, passing a value type (like
Int32) to a non-generic interface (likeIComparable) forced the CLR to box the value into an object on the heap, destroying performance. Generic interfaces (likeIComparable<T>) accept the value type directly by value, completely eliminating the boxing penalty,. - Multiple Implementations on a Single Class: A single class can implement the same generic interface multiple times. For example, a
Numberclass can implement bothIComparable<Int32>andIComparable<String>, allowing it to sort itself against integers and strings using entirely different logic.
T. By default, it treats T as a basic System.Object.where T : IComparable<T>, you are promising the compiler that whatever type is passed in will definitely implement that interface. You can even specify multiple interface constraints on a single generic parameter (where T : IWindow, IRestaurant). This requires the passed type to implement all specified interfaces, allowing your generic method to confidently call methods from any of those interfaces.MarioPizzeria class that implements both IWindow and IRestaurant, and both interfaces happen to require a method named Object GetMenu()?public Object GetMenu(), the compiler will map both interfaces to the same method. But a window menu is very different from a restaurant menu! To solve this naming collision, you must use Explicit Interface Method Implementations (EIMIs).Object IWindow.GetMenu() and Object IRestaurant.GetMenu(). Callers using the MarioPizzeria object must cast the object to either IWindow or IRestaurant to specify exactly which menu they want to retrieve,.IComparable) to ensure backward compatibility with legacy .NET Framework methods,.IComparable.CompareTo accepts a System.Object, any value type passed into it will be boxed. You can mitigate this by explicitly implementing the non-generic interface method (making it private via EIMI) and then exposing a public, strongly-typed method for callers who know the exact type. This hides the weakly-typed Object version from public view and funnels callers toward the fast, type-safe, non-boxing implementation,.- No Documentation or IntelliSense: Because EIMIs are technically private, they do not show up in Visual Studio's IntelliSense when you dot into an object, confusing developers who expect the method to be there,.
- Boxing Traps: If an EIMI is implemented on a value type, the caller must cast the value type to the interface to invoke the method. Casting a value type to an interface forces a boxing allocation, hurting performance,.
- Derived Class Nightmares: Because an EIMI is not
publicorvirtual, a derived class cannot easily override or call the base class's implementation. If a derived class caststhisto the interface and calls the method, it will invoke its own interface implementation, triggering an infinite recursion loop,,.- The Fix: If you must use an EIMI and want derived classes to customize the behavior, the base class must provide a separate,
virtualmethod that the EIMI forwards its calls to. Derived classes can then override that virtual method,.
- The Fix: If you must use an EIMI and want derived classes to customize the behavior, the base class must provide a separate,
- IS-A vs. CAN-DO: A class can inherit only one base class. If a type isn't strictly an "IS-A" derivative of the base class, use an interface. Interfaces denote a "CAN-DO" relationship (e.g.,
IConvertible,ISerializable). Also, since Value Types cannot inherit from arbitrary base classes, you must use interfaces if you want structs to share a polymorphic contract. - Ease of Use: Base classes are dramatically easier for developers to consume. A base class can provide default implementation logic, meaning the derived class only has to tweak what it needs to. With an interface, the consumer must write all the boilerplate code from scratch.
- Consistent Implementation: Interfaces are pure contracts; they don't guarantee that the implementer wrote the code correctly. A base class ensures the core logic is centralized, tested, and correct out of the box.
- Versioning: This is critical. If you add a new method to a base class, derived classes automatically inherit it without breaking—no recompilation needed. If you add a new method to an interface, you instantly break every class in the world that implements that interface, because those classes now lack a required implementation.
- The Hybrid Approach: The best of both worlds is often to provide both. You can define an interface (e.g.,
IComparer<T>) to establish the contract, and simultaneously provide an abstract base class (e.g.,Comparer<T>) that provides a robust default implementation. This gives consumers the ultimate flexibility to choose the approach that best fits their architecture.
System.Char structure, a lightweight value type .System.Char type is quite simple, providing two public read-only constant fields: MinValue (defined as '\0') and MaxValue (defined as '\uffff') . When you have an instance of a Char, you can interact with it using methods like the static GetUnicodeCategory, which returns a System.Globalization.UnicodeCategory enum . This enum allows you to identify whether the character is a math symbol, punctuation mark, currency symbol, lowercase letter, uppercase letter, or control character as defined by the Unicode standard .Char to a numeric type, there are a few techniques, but you must be aware of their performance and data loss implications:- The Convert Type: The
System.Convertclass offers several static methods to convert aCharto a numeric type and vice versa . These are checked operations; if the conversion would result in data loss, the CLR throws aSystem.OverflowException. - The IConvertible Interface: The
Chartype and all FCL numeric types implementIConvertible, which defines methods likeToUInt16andToChar. However, this is the least efficient technique because calling an interface method on a value type requires the CLR to box the instance . If a conversion is invalid or results in data loss, anInvalidCastExceptionis thrown . Additionally, many types implement these methods explicitly, meaning you must explicitly cast your instance toIConvertiblebefore invoking the method .
System.String class is one of the most frequently used types in any application . A String represents an immutable sequence of characters . Because it derives directly from Object, String is a reference type, which means that string objects (and their underlying character arrays) always reside on the managed heap, never on the thread's stack . The String type also implements several core interfaces, including IComparable, ICloneable, IConvertible, IEnumerable, and IEquatable<String> .String as a primitive type, allowing you to express literal strings directly in your source code . When the C# compiler processes a literal string, it embeds it into the module's metadata . Interestingly, if you examine the Intermediate Language (IL) code, you will not see the newobj instruction used to construct standard objects . Instead, the CLR uses a special ldstr (load string) IL instruction to construct a String object directly from the literal string obtained from the metadata .+ operator. If you concatenate literal strings (e.g., "Hi" + " " + "there."), the C# compiler performs the concatenation at compile time and places just one single string in the module's metadata . However, using the + operator on nonliteral (variable) strings causes the concatenation to occur at runtime . You should avoid using the + operator to concatenate multiple strings at runtime because it creates multiple intermediate string objects on the garbage-collected heap; you should use System.Text.StringBuilder instead .@ symbol . This instructs the compiler to treat backslash characters as literal backslashes rather than escape characters, which makes file paths and regular expressions much more readable in your source code .System.String class . The CLR knows the exact internal layout of the fields defined within String and accesses them directly for maximum performance . Consequently, the String class is heavily optimized and sealed . If developers were permitted to derive their own types from String, they could add new fields or alter behaviors, breaking the CLR’s strict assumptions about string immutability and memory layout .System.Globalization.CultureInfo type to represent a specific language and country pair (e.g., "en-US" or "de-DE") . Every thread maintains two properties: CurrentUICulture (used for loading UI resources) and CurrentCulture (used for number formatting, date formatting, string casing, and string comparisons) ,.CultureInfo object has a field referring to a System.Globalization.CompareInfo object, which encapsulates the culture's specific character-sorting tables as defined by the Unicode standard ,. By using CompareInfo directly, you can pass bit flags from the CompareOptions enum to gain precise control over string comparisons .,. If the same literal string appears multiple times in your source code, the compiler writes it into the module's metadata only once . All code references are then modified to point to this single metadata string .String object in memory . This significantly conserves memory usage across your application . However, string interning requires internal hash table lookups, so the C# compiler automatically applies the CompilationRelaxationsAttribute with the NoStringInterning flag to assemblies, allowing the CLR to opt out of interning all strings to improve performance .Char does not always equate to an abstract Unicode character . Some characters are a combination of two code values, such as an Arabic letter combined with an Arabic Kasra below it, which together form a single abstract text element . Furthermore, some Unicode characters require more than 16 bits to represent them, resulting in a surrogate pair . A surrogate pair consists of a high surrogate (U+D800 to U+DBFF) and a low surrogate (U+DC00 to U+DFFF), allowing Unicode to express over a million characters .System.Globalization.StringInfo type . By constructing a StringInfo instance, you can safely query the LengthInTextElements property or extract exact elements using the SubstringByTextElements method without corrupting the string .String type offers methods for copying, such as Clone (which simply returns a reference to the same object because strings are immutable) and Copy (which makes an actual duplicate array in memory, though this is rarely used) ,. It also provides a myriad of manipulation methods like Insert, Remove, PadLeft, Replace, Split, ToLower, and Format . Remember: because strings are immutable, all of these manipulation methods allocate and return brand new string objects .String type is immutable, performing extensive dynamic text operations can result in enormous memory pressure and poor performance. To solve this, the Framework provides the System.Text.StringBuilder class .StringBuilder as a highly efficient, fancy constructor used exclusively to build a String . Logically, a StringBuilder object maintains a private array of Char structures . As you call methods to append, insert, or replace characters, you are mutating this internal array directly . If you grow the string past its currently allocated capacity, the StringBuilder automatically allocates a larger array, copies the characters over, and discards the old array to be garbage collected .String, the CLR has no special knowledge of StringBuilder, so you construct it using the standard new operator . The StringBuilder allocates a new object on the managed heap on only two occasions: when you exceed its capacity, and when you finally call ToString() to extract the completed String object .StringBuilder offers members like Capacity, EnsureCapacity, Length, Append, Insert, AppendFormat, Replace, and Remove -. Crucially, most of these methods return a reference to the exact same StringBuilder object . This design enables a convenient syntax where you can fluently chain several operations together in a single statement, such as sb.AppendFormat(...).Replace(...).Remove(...) .StringBuilder does not offer complete method parity with String . It lacks methods like ToLower, ToUpper, EndsWith, and Trim . To accomplish these tasks, you are forced to call ToString(), manipulate the resulting String, clear the StringBuilder (by setting its Length to 0), and then append the modified string back in ,.System.Object defines a public, virtual, parameterless ToString method . By default, Object.ToString() simply returns the full name of the object's type .ToString to populate the datatips when you hover over variables .ToString method relies on the calling thread's current culture and offers the caller no control over formatting . To fix this, types can implement the IFormattable interface . IFormattable.ToString takes two parameters: a string format and an IFormatProvider .null for the format string, it defaults to the "G" general format .IFormatProvider dictates the culture-specific rules applied during formatting . The FCL defines three main types that implement IFormatProvider: CultureInfo, NumberFormatInfo, and DateTimeFormatInfo . When you format a number, the ToString method will query the provider for a NumberFormatInfo object, which defines specific cultural properties like CurrencySymbol or NegativeSign .String.Format (or StringBuilder.AppendFormat) ,. These methods allow you to specify replaceable parameters in braces, such as On {0:D}, {1} is {2:E} years old. .IFormatProvider and ICustomFormatter, you can pass your class directly to String.Format ,. As the string is evaluated, your class's Format method is called for every single replaceable parameter, giving you the ultimate flexibility to intercept and modify the output—such as automatically wrapping all Int32 values in HTML <B> tags before they are appended to the final string ,.ToString converts an object to a string, the framework needs a way to do the exact opposite. Microsoft formalized this mechanism through the Parse methods .Parse method, such as Int32.Parse, which takes a String representing a number, a NumberStyles bit-flag enum identifying the acceptable characters (like leading whitespace or currency symbols), and an IFormatProvider indicating the culture . If the string cannot be parsed correctly, Parse throws an exception . Alternatively, to avoid expensive exception handling, you can use the TryParse method, which returns a Boolean true or false to safely indicate success or failure .System.Text.Encoding .- UTF-16 (Unicode encoding): Encodes every character as exactly 2 bytes . Because no compression occurs, its performance is excellent .
- UTF-8: An extremely popular encoding that compresses characters into 1, 2, 3, or 4 bytes . Standard US characters take 1 byte, European characters take 2, East Asian characters take 3, and surrogate pairs take 4 . While heavily used, it is less efficient than UTF-16 if your data contains many East Asian characters .
GetPreamble method, which returns the Byte Order Mark (BOM) identifying the encoding format . They also provide the Convert method to easily translate an array of bytes from one encoding to another .NetworkStream in unpredictable chunks . If you receive 5 bytes of a UTF-16 stream, the final byte may be half of a character . If you attempt to decode these bytes immediately using Encoding.GetString, the final character will be corrupted .Encoding classes do not maintain state between calls, you must instead use Decoder and Encoder objects ,. You obtain these by calling the encoding object's GetDecoder or GetEncoder methods . These stateful objects hold onto any leftover bytes from the previous chunk and seamlessly combine them with the next chunk to guarantee perfect, lossless character reconstruction ,.String, that string lives unencrypted in the managed heap . Even if you are done with it, its characters will not be zeroed out until a garbage collection reclaims the memory, leaving a dangerous window of opportunity for malicious code to read the sensitive data .System.Security.SecureString class . The SecureString type encrypts the string's contents in memory and avoids exposing the sensitive data by purposefully choosing not to override the ToString method .SecureString (such as WPF's PasswordBox or when interacting with Cryptographic Service Providers), you often have to extract the data manually ,. To access the decrypted string, you must use the System.Runtime.InteropServices.Marshal class . You call methods like SecureStringToBSTR or SecureStringToCoTaskMemUnicode to decrypt the characters into a temporary unmanaged memory buffer . You must then use this unmanaged buffer as quickly as possible and immediately call the corresponding ZeroFree method (like ZeroFreeCoTaskMemUnicode) to obliterate the cleartext data from RAM, keeping the security window tightly closed ,Color enum where White equals 0, Red equals 1, and Green equals 2.- Readability and Maintainability: Your code uses meaningful symbolic names rather than arbitrary, hard-coded "magic numbers" that developers have to mentally translate.
- Easy Refactoring: If the underlying numeric value of a symbol ever needs to change, you simply update the enum definition and recompile; you don't have to hunt down every instance of the number "1" in your source code.
- Tooling Support: Debuggers and documentation tools can display meaningful string names instead of raw integers, vastly improving the debugging experience.
System.Enum class, which derives from System.ValueType, which ultimately derives from System.Object. This means that enums are value types. They can be boxed and unboxed just like any other value type.Color.Red enum defined in Assembly B, Assembly A's compiled IL just contains the hard-coded number 1. At runtime, Assembly A doesn't even need Assembly B to load (unless it explicitly references the enum type itself). If the publisher of Assembly B changes Color.Red to equal 5 and ships a new DLL, Assembly A will still use 1 until it is explicitly recompiled against the new version of Assembly B.System.Enum Methods (and Their Pitfalls)System.Enum, they inherit several incredibly useful static and instance methods:- GetValues / GetEnumValues: These methods allow you to dynamically retrieve an array of all the values defined in an enum. Because they return a base
Arraytype, you must cast the result to your specific array type. - ToObject: Offers a suite of static methods to convert numeric primitive types (like
Byte,Int32,Int64, etc.) into an instance of an enumerated type. - IsDefined: This method checks if a specific numeric value or string exists within the enum.
IsDefined.- It is case-sensitive and cannot be forced into a case-insensitive search.
- It uses reflection under the hood, making it quite slow.
- It creates a versioning and security risk: If you write a method that checks
IsDefinedto validate input, and the enum's publisher later adds a new value (likePurple), your method will suddenly acceptPurpleas valid input—even if your internal code was never designed to handle it.
System.IO.FileAttributes type, where a single file might simultaneously be ReadOnly, Hidden, and a System file.| for OR and & for AND) to combine or query these states. Note that your symbols do not have to be pure powers of two; you can define a convenience symbol like All = 0x001F that represents a combination of several other bits.[Flags] Attribute and Formatting[System.Flags] custom attribute to it. This attribute drastically changes how the ToString() method behaves.ToString() on an enum value of 3 without the [Flags] attribute, and no single symbol maps to 3, the method simply returns the string "3".[Flags] attribute is present, ToString() performs a clever algorithm:- It obtains all the numeric values defined in the enum and sorts them in descending order.
- It performs a bitwise-AND against the instance's value. If the result equals the symbol's value, it appends that symbol's string name to the output and subtracts that value from the running total.
- It ultimately returns a beautifully formatted, comma-separated string like
"Read, Write".
[Flags] attribute applied, you can force it by passing the "F" (Flags) format string into the ToString("F") method. If a numeric value contains a bit that simply doesn't map to any defined symbol, the output string will just contain the raw decimal number.- The HasFlag Performance Trap: The .NET Framework provides a convenient
HasFlagmethod to check if a specific bit is set. However, because this method takes a baseEnumparameter, any value you pass to it must be boxed. This requires a memory allocation on the managed heap, which can hurt your application's performance and trigger unnecessary garbage collections. It is vastly more performant to stick to standard bitwise math (e.g.,(actions & Actions.Read) == Actions.Read). - IsDefined Fails with Bit Flags: Never use the
Enum.IsDefinedmethod to validate bit flag combinations.IsDefinedonly checks if a single symbol's numeric value matches the passed-in number. Because bit flags are combined values,IsDefinedwill almost always returnfalsefor combinations.
Set and Clear extension methods. This allows you to write highly readable code that looks exactly as if the methods were natively built into the enum itself:FileAttributes fa = FileAttributes.System;
fa = fa.Set(FileAttributes.ReadOnly);
fa = fa.Clear(FileAttributes.System);
- Value Type Arrays: If you allocate
new Int32, the CLR allocates a single memory block on the managed heap large enough to hold 100 unboxed 32-bit integers, all initialized to 0. - Reference Type Arrays: If you allocate
new Control, the CLR allocates a memory block for 50 references (pointers), all initialized tonull. The actualControlobjects are not created until you explicitly instantiate them and assign them to the array indices.
System.IndexOutOfRangeException. While bounds checking incurs a slight performance penalty, the Just-In-Time (JIT) compiler optimizes this by checking array bounds once before a loop executes, rather than at every single iteration.- Vectors: Single-dimensional, zero-based arrays (sometimes called SZ arrays). These offer the absolute best performance because the CLR utilizes highly optimized IL instructions (like
newarr,ldelem,stelem) to manipulate them. - Multi-Dimensional Arrays: Rectangular arrays (e.g.,
Double[,]). - Jagged Arrays: Arrays of arrays (e.g.,
Point[][]). While accessing elements in a jagged array requires multiple memory lookups, zero-based single-dimensional jagged arrays often perform better than multi-dimensional arrays due to vector optimizations.
var) alongside implicitly typed arrays (new[]) to initialize an array of anonymous objects on the fly.foreach loop without losing compile-time type safety.String[] can be cast to an Object[] because String derives from Object.Int32[] directly to a Double[].System.Array class provides a highly optimized Array.Copy method. The Array.Copy method is incredibly versatile and can be used to:- Unbox reference type elements into value types (e.g., copying an
Object[]to anInt32[]). - Widen CLR primitive value types (e.g., copying an
Int32[]to aDouble[]). - Downcast elements when casting between arrays that cannot be proven compatible based on their type alone (e.g., copying an
Object[]to anIFormattable[], which will succeed only if every object inside implementsIFormattable).
System.Array abstract base class, your arrays automatically inherit a suite of powerful instance methods and properties. Without writing any extra code, your arrays gain access to methods like Clone, CopyTo, GetLength, GetLongLength, GetLowerBound, GetUpperBound, and properties like Length and Rank.System.Array itself to implement generic interfaces like IEnumerable<T>, ICollection<T>, and IList<T> because that would force these interfaces onto multi-dimensional and non-zero-based arrays, where they conceptually do not fit.FileStream[] will automatically implement IList<FileStream>, but also IList<Stream> and IList<Object>. This allows you to seamlessly pass a FileStream[] to any method expecting a collection of Stream or Object types.DateTime[]), the array will only implement the interfaces strictly for that exact value type (e.g., IList<DateTime>). It will not implement IList<System.ValueType> or IList<System.Object> because value type arrays have a fundamentally different memory layout than reference type arrays, making polymorphic casting impossible without boxing.Array.Copy performs a shallow copy, so reference type elements will still point to the same objects in the heap).null when a method has no elements to return. Returning null forces the caller to litter their code with null-checking logic before iterating. Always return an array with zero elements instead of null. This allows the caller to gracefully execute foreach loops or Length checks without risking a NullReferenceException, making APIs much easier to consume.Array.CreateInstance. This method allows you to explicitly define the element type, the number of dimensions, the lower bounds for each dimension, and the lengths of each dimension.Decimal[,]) for easier syntax, single-dimensional non-zero-based arrays in C# must be accessed using the slower GetValue and SetValue methods because C# lacks the syntax to access them directly.- SZ Arrays (Vectors): Single-dimensional, zero-based arrays.
- Unknown Lower Bound Arrays: Multi-dimensional arrays and single-dimensional arrays with an unknown (or non-zero) lower bound.
.GetType(), you will see a fascinating distinction. An SZ array returns System.String[], while a 1-dimensional array with a lower bound of 1 returns System.String[*]. The * signifies to the CLR that the array is not necessarily zero-based.[*] array is substantially slower than accessing an SZ array. For SZ arrays, the JIT compiler aggressively optimizes the code by hoisting index boundary checks outside of loops. For multi-dimensional arrays, the JIT compiler cannot perform this hoisting, meaning it must validate the indices and subtract the array's lower bounds from the specified index on every single iteration, even if the multi-dimensional array happens to be zero-based.unsafe modifier and the fixed statement, you can pin a managed array in the heap (preventing the garbage collector from moving it) and iterate over its elements using raw pointers. While this is incredibly fast, it comes with severe downsides:- Complexity: Pointer arithmetic is harder to read, write, and maintain.
- Risk: A single math error can lead to accessing memory outside the array bounds, causing silent data corruption, type-safety violations, and severe security holes.
- Security Restrictions: Because of these risks, the CLR strictly forbids unsafe code from running in reduced-security environments (such as Silverlight).
stackalloc Statementstackalloc statement. This acts much like the C alloca function. stackalloc is limited to single-dimensional, zero-based arrays containing value types that have no reference type fields. Because it bypasses the heap, allocation is virtually instantaneous, and the memory is reclaimed immediately when the method returns.unsafe and fixed keywords together, you can create an inline array directly inside a structure. To do this, the struct must meet strict requirements: the type must be a value type, the array must be single-dimensional and zero-based, and the array element must be a specific core primitive type (like Char, Int32, Double, etc.). This technique is primarily utilized for unmanaged interoperability but offers another powerful tool for managing exact memory layouts in C#.qsort. However, in unmanaged C/C++, a callback is simply a raw memory address. This memory address carries zero information about the number of parameters the function expects, the parameter types, or the return type, making unmanaged callbacks inherently not type-safe.delegate keyword. For example, declaring internal delegate void Feedback(Int32 value); creates a delegate type that strictly identifies a method taking one Int32 parameter and returning void. You can think of a delegate as being very similar to an unmanaged C/C++ typedef representing a function pointer, but with robust type-safety built in.Counter method could iterate through a sequence of integers and call the Feedback delegate for each item being processed.null as the delegate argument. To actually invoke a static callback, you construct a new delegate object using the new operator, passing in the name of the static method you want to wrap. For example, new Feedback(Program.FeedbackToConsole) wraps the FeedbackToConsole static method inside the Feedback delegate object.Int32) because value types and reference types have fundamentally different memory structures.Program object named p, you can create a delegate using new Feedback(p.FeedbackToFile). The delegate wraps the reference to the FeedbackToFile instance method, and when the callback is invoked, the address of the p object is passed as the implicit this argument to the method.delegate, construct them with new, and invoke them like a standard method. However, the C# compiler and the Common Language Runtime (CLR) are doing a tremendous amount of heavy lifting behind the scenes to hide the complexity.internal delegate void Feedback(Int32 value);, the C# compiler automatically generates a complete class definition. This compiler-generated class derives from the System.MulticastDelegate type defined in the Framework Class Library (FCL), which itself derives from System.Delegate and ultimately System.Object. The generated class contains four crucial methods: a constructor, Invoke, BeginInvoke, and EndInvoke.MulticastDelegate, they inherit three highly significant non-public fields:_target(System.Object): This holds a reference to the object instance the method should operate on, or it remainsnullif wrapping a static method._methodPtr(System.IntPtr): This is an internal integer used by the CLR to identify the specific method to be called._invocationList(System.Object): This field is usuallynullbut can refer to an array of delegates when building a delegate chain (which we will cover next).
_target and _methodPtr fields.fb(val)), the compiler actually generates Intermediate Language (IL) code to explicitly call the delegate object's Invoke method. The Invoke method uses the internal _target and _methodPtr fields to successfully dispatch the call to the desired method on the specified object.Delegate class's public, static Combine method. When you combine a null delegate reference with a valid delegate, Combine simply returns the valid delegate. However, if you call Combine on a delegate that already refers to an object, Combine constructs an entirely new delegate object. This new delegate initializes its _invocationList field to an array of delegates, containing both the original delegate and the newly appended delegate.Invoke method just as it normally would. The delegate inspects its _invocationList field, sees the array, and iterates through all the elements, calling each wrapped method sequentially. It is important to note that if these callback methods return values, only the result of the last delegate called is actually returned to the invoker; all previous return values are discarded.+= and -= operators. Using += automatically emits a call to Delegate.Combine, while -= emits a call to Delegate.Remove.GetInvocationList method. This method returns an array of Delegate references, allowing you to explicitly iterate over each callback, catch individual exceptions, and aggregate all return values exactly as you see fit.MSCorLib.dll assembly alone contains nearly 50 distinct delegate types like WaitCallback and TimerCallback.Object parameter and returned void. With the introduction of Generics to the .NET Framework, creating dozens of custom delegate types is no longer necessary.Action delegates (which return void) and 17 generic Func delegates (which return a value). These built-in generic delegates support anywhere from 0 to 16 parameters. Microsoft highly recommends using these pre-defined Action and Func delegates wherever possible to simplify coding and reduce type bloat in the system. The only time you should really define your own custom delegate today is if you need to pass an argument by reference using the ref or out keywords.new (e.g., button1.Click += new EventHandler(button1_Click)). Most programmers prefer simpler syntax, and fortunately, the C# compiler offers several shortcuts that generate the necessary underlying IL automatically.new operator, you can simply assign the method name directly, like button1.Click += button1_Click;. The C# compiler provides even deeper syntactical sugar through Lambda Expressions (and their predecessor, Anonymous Methods).=> operator. When the compiler encounters a lambda expression, it secretly generates a private method (often inside a compiler-generated class) that contains your callback code.System.Reflection.MethodInfo class provides a CreateDelegate method that allows you to construct a delegate dynamically.Type of the delegate you want to create and, if the method is an instance method, a reference to the target object (this) to CreateDelegate. Once you have successfully constructed the delegate dynamically, you can invoke it using the Delegate class's DynamicInvoke method, passing in an array of objects that represent the arguments.public, private, and static are built-in attributes that we apply to types and members every day. But what if we want to define our own? Compiler vendors are generally hesitant to release their source code to let developers add custom keywords, so Microsoft introduced a generalized mechanism called custom attributes. Anyone can define and use custom attributes, and all compilers targeting the CLR are required to recognize them and emit them into the resulting metadata.- The
[DllImport]attribute informs the CLR that a method's implementation lives in unmanaged code. - The
[Serializable]attribute tells serialization formatters that an object's fields can be serialized and deserialized. - The
[Flags]attribute applied to an enumeration alters its behavior to act as a set of bit flags.
[assembly: SomeAttr] applies an attribute to the entire assembly, while [return: SomeAttr] explicitly applies it to a method's return value,.[Serializable][Flags]) or by comma-separating them within one set of brackets (e.g., [Serializable, Flags]),.System.Attribute. By standard convention, the class name should end with the "Attribute" suffix, though this is not strictly mandatory. C# generously allows you to omit the "Attribute" suffix in your source code for brevity when applying it to targets.System.AttributeUsageAttribute,. This attribute accepts a bit flag from the System.AttributeTargets enumeration, allowing you to restrict your attribute to targets like AttributeTargets.Enum or AttributeTargets.Class | AttributeTargets.Method,.AttributeUsageAttribute class also offers two highly important optional properties:- AllowMultiple: Determines if your attribute can be applied more than once to a single target. By default, this is
false. For example, you cannot apply[Flags]twice to the same enum. However, attributes likeConditionalAttributeset this totrueto allow multiple conditional evaluations. - Inherited: Indicates whether the attribute should automatically be applied to derived classes or overriding methods,. By default, this is
true. Keep in mind that the .NET Framework only considers classes, methods, properties, events, fields, method return values, and parameters to be inheritable targets.
System.Reflection.CustomAttributeExtensions class provides three primary static extension methods for retrieving attributes:- IsDefined: Returns
trueif at least one instance of the attribute is associated with the target. This method is highly efficient because it checks the metadata without actually constructing or deserializing the attribute object,. - GetCustomAttributes: Returns a collection of the specified attribute objects applied to the target. Every time you call this, it deserializes the metadata and constructs new instances of the attribute classes,. This is commonly used when
AllowMultipleistrue. - GetCustomAttribute: Constructs and returns a single instance of the specified attribute class, returning
nullif none exist, or throwing aSystem.Reflection.AmbiguousMatchExceptionif multiple instances are found.
Attribute, Type, and MethodInfo classes implement reflection methods that honor the inherit parameter to check the inheritance hierarchy. If you need to check for inherited attributes on events, properties, fields, constructors, or parameters, you must call the methods defined directly on the System.Attribute class. Furthermore, when you search for an attribute, the system will also return any attributes derived from the specific class you requested.System.Attribute base class overrides Object's Equals method to perform this check. By default, Equals uses reflection to compare the values of all fields in the two attribute objects. If performance is critical, you should override Equals in your custom attribute to bypass the slow reflection mechanism.Match simply calls Equals, you can override it to execute complex logic. For example, if you have an AccountsAttribute that stores a bit-flag enumeration, you can override Match to return true if one attribute's bit flags represent a subset of the other attribute's bit flags, rather than requiring an exact match.GetCustomAttribute(s), the CLR actually calls the attribute class's constructor and property setter methods. This allows unknown, potentially malicious code to execute inside your AppDomain simply because you looked for an attribute.System.Reflection.CustomAttributeData class. You typically use this in conjunction with Assembly.ReflectionOnlyLoad, which loads an assembly strictly for metadata parsing and explicitly prevents the CLR from executing any code (including static type constructors) within it.CustomAttributeData.GetCustomAttributes, it acts as a factory, returning a collection of CustomAttributeData objects. You can securely query these objects using read-only properties:- Constructor: Indicates which constructor method would be called.
- ConstructorArguments: Returns an
IList<CustomAttributeTypedArgument>representing the positional arguments that would be passed to the constructor. - NamedArguments: Returns an
IList<CustomAttributeNamedArgument>representing the fields/properties that would be set.
[SuppressMessage] attribute used by Visual Studio's FxCop code analysis tool), assemblies can become bloated. When your application runs in production, these development-only attributes just sit in metadata, making the file larger, increasing the process's working set, and hurting performance.System.Diagnostics.ConditionalAttribute to your custom attribute class,. When an attribute class is marked as conditional, the compiler will only emit the attribute into the metadata of the target if the specified symbol (e.g., "DEBUG", "TEST", or "VERIFY") is defined at compile time,. If the symbol is not defined, the compiler completely ignores the application of the attribute, keeping your release builds lean and optimizednull to indicate they don't point to a valid object, but value type variables always contain a value of their underlying type, with all members initialized to 0 by default. Because value types are not pointers, it is impossible for them to be natively null.System.Nullable<T> structure. Because Nullable<T> is a value type itself, it does not add the performance overhead of heap allocations and garbage collection.Nullable<T>, you can determine if the variable holds a real value or if it is logically "null" by querying its properties. Consider the following code:Nullable<Int32> x = 5;
Nullable<Int32> y = null;
Console.WriteLine("x: HasValue={0}, Value={1}", x.HasValue, x.Value);
Console.WriteLine("y: HasValue={0}, Value={1}", y.HasValue, y.GetValueOrDefault());
x: HasValue=True, Value=5 y: HasValue=False, Value=0null to a Nullable<T> is perfectly legal. Under the hood, setting it to null simply sets its internal boolean flag (represented by HasValue) to false. When HasValue is false, attempting to read the Value property will throw an exception, which is why the code safely uses GetValueOrDefault() for variable y instead.Nullable<Int32> x = 5;, you can simply write Int32? x = 5;. To the compiler, Int32? is exactly identical to Nullable<Int32>.Point struct) and explicitly overload operators like == and !=, the C# compiler is smart enough to handle nullable versions of your struct. If you compare two Point? variables, the compiler seamlessly unwraps the nullable types, checks their HasValue properties to ensure they are valid, and then gracefully invokes your custom overloaded operators.??)if/else statements every time you need to check for nulls makes code bloated and hard to read.?? operator shines brightest when used in modern C# constructs like lambda expressions. Look at how concise this delegate definition is:Func<String> f = () => SomeMethod() ?? "Untitled";
Func<String> f = () => {
var temp = SomeMethod();
return temp != null ? temp : "Untitled";
};
?? operator provides is composability. Because it can be chained, you can easily set up a priority list of fallback values in a single, highly readable line of code:String s = SomeMethod1() ?? SomeMethod2() ?? "Untitled";
if/else blocks you would otherwise have to write:String s;
var sm1 = SomeMethod1();
if (sm1 != null) s = sm1;
else {
var sm2 = SomeMethod2();
if (sm2 != null) s = sm2;
else s = "Untitled";
}
Nullable<T> to ensure it behaves predictably and efficiently.Int32? and you want to pass it to a method that requires an IComparable interface. Under normal circumstances, value types must be boxed into the managed heap to be treated as an interface. However, the CLR provides special support for casting a nullable value type directly to an interface implemented by its underlying type.Int32? n = 5;
Int32 result = ((IComparable) n).CompareTo(5); // Compiles & runs OK
Console.WriteLine(result); // Displays 0
Int32 result = ((IComparable) (Int32) n).CompareTo(5); // Cumbersome
struct constraint to a generic parameter promises the compiler that the type argument will be a value type.Nullable<T> is a value type, you might expect it to satisfy the struct constraint. However, the compiler and the CLR treat System.Nullable<T> as a special exception: nullable types do not satisfy the struct constraint.Nullable<T> type definition internally constrains its own type parameter T to struct. If the CLR allowed you to pass a nullable type into a struct constraint, it would open the door for developers to create bizarre, recursive types like Nullable<Nullable<T>>. By explicitly failing the struct constraint, the CLR prevents this logical impossibility and ensures the type system remains safe and stable.Read, Write, or Flush."Jeff".Substring(1, 1).ToUpper().EndsWith("E")). However, this assumes that no operation fails. When an operation does fail, the framework needs a reliable way to report it without relying on clumsy error codes, and that mechanism is exception handling.- The try Block: A try block contains code that requires common cleanup operations, exception-recovery operations, or both. A
tryblock must be associated with at least onecatchorfinallyblock. - The catch Block: This block contains code to execute in response to an exception. You specify a catch type (which must be
System.Exceptionor a derived type), and the CLR searches from top to bottom for a matching catch type. Once a match is found, you have three choices: re-throw the exact same exception, throw a new exception with richer context, or let the thread fall out of the bottom of the catch block to continue execution. - The finally Block: A finally block contains code that is guaranteed to execute. This is typically where you perform necessary cleanup operations, such as closing a file or releasing a lock.
System.Exception. While the CLR technically allows any object (like an Int32 or String) to be thrown, C# only allows throwing Exception-derived objects. To prevent security vulnerabilities where C# code might fail to catch non-CLS exceptions thrown by other languages, the CLR automatically wraps all non-CLS exceptions in a RuntimeWrappedException.System.Exception, it acts as the foundation for error reporting. It contains several critical properties:- Message: Helpful text indicating why the exception was thrown. It should be highly technical to assist developers in fixing the code.
- Data: A collection of key-value pairs where the throwing code can add contextual information.
- Source: The name of the assembly that generated the exception.
- StackTrace: Contains the names and signatures of methods called that led up to the exception. This property is invaluable for debugging.
- TargetSite: The method that threw the exception.
- HelpLink: A URL pointing to documentation about the exception.
throw e;, the CLR resets the starting point of the exception, losing the original stack trace. If you want to re-throw the exception and preserve the original stack trace, you must use the throw; keyword by itself.System.SystemException and all application-thrown exceptions to derive from System.ApplicationException.ApplicationException, and some application exceptions derive from SystemException. Because it became a mess, the SystemException and ApplicationException types have no special meaning at all, but they remain in the framework for backward compatibility.Exception-derived types should be serializable (to cross AppDomain boundaries). This requires implementing the ISerializable interface, special serialization constructors, and security attributes.Exception<TExceptionArgs> class. By defining a simple ExceptionArgs class for your specific error data, you can trivially throw custom exceptions like throw new Exception<DiskFullExceptionArgs>(...) without having to manually implement serialization logic for every new error type.OutOfMemoryException, a TypeInitializationException, or an OverflowException. If your code is mutating state when one of these unexpected exceptions occurs, your application's state becomes corrupted. State corruption is not just a bug; it is a serious security vulnerability.- Use
finallyblocks to perform sensitive state mutations (since the CLR prevents thread aborts withinfinallyblocks). - Use Code Contracts to validate arguments before mutating state.
- Use Constrained Execution Regions (CERs) to prepare code in advance.
- Use transactions to ensure all state is modified or none is.
- Call
System.Environment.FailFastto immediately terminate the process if you detect that state is corrupted beyond repair.
- Use finally blocks liberally: They are perfect for ensuring resources are cleaned up or explicitly disposed, regardless of whether an operation succeeded or failed. C# statements like
using,lock, andforeachautomatically generatefinallyblocks for you. - Don't Catch Everything: A type that’s part of a class library should never, ever, under any circumstance catch and swallow all exceptions. Catching
System.Exceptionhides failures, leading to unpredictable results. - Back out of partially completed operations: If an operation fails halfway through (e.g., serializing objects to a file), catch all exceptions, restore the data to its original state, and then re-throw the exception to let the caller know it failed.
- Hide implementation details: Sometimes you want to catch one exception and throw a different one to maintain a method's contract (e.g., catching a
FileNotFoundExceptionand throwing aNameNotFoundException). When doing this, always set theInnerExceptionproperty so the original stack trace isn't lost. Be careful, though, as this obscures where the error actually occurred.
AppDomain.UnhandledException or Application.DispatcherUnhandledException).catch or finally blocks. You can override this by applying the HandleProcessCorruptedStateExceptionsAttribute.catch block.Int32.TryParse). These methods return a Boolean indicating success or failure, avoiding the massive performance hit of throwing an exception. Remember: A TryXxx method returns false to indicate one and only one type of failure; it should still throw exceptions for other failures like OutOfMemoryException.ThreadAbortException or OutOfMemoryException).RuntimeHelpers.PrepareConstrainedRegions() immediately before a try block.catch and finally blocks. It loads required assemblies, JIT-compiles methods, and runs static constructors before the thread enters the try block. If any of these preparation steps fail, the exception is thrown before your state mutation begins, guaranteeing that your cleanup code won't fail due to lazy-loading mechanisms.ReliabilityContractAttribute to document to the CLR and callers whether your method promises not to corrupt state (Consistency.WillNotCorruptState) and whether it might fail (Cer.MayFail or Cer.Success).- Preconditions: Validate arguments before a method executes.
- Postconditions: Validate state when a method terminates.
- Object Invariants: Ensure an object's fields remain in a valid state throughout its lifetime.
System.Diagnostics.Contracts.Contract class, using methods like Contract.Requires(), Contract.Ensures(), and Contract.Invariant().return point in your method. To reduce assembly bloat, you can also use CCRefGen.exe to strip the implementation and produce a metadata-only contract reference assembly.Contract.ContractFailed event is raised, allowing the application to log the failure, ignore it, or throw a ContractException- Allocate memory for the type (using the
newoperator). - Initialize the memory to set the resource's initial state (via the constructor).
- Use the resource by accessing its members.
- Tear down the state of the resource.
- Free the memory.
NextObjPtr—that indicates where the next object will be allocated.new operator, the CLR calculates the bytes required for the type's fields, adds the overhead for the type object pointer and sync block index, and allocates the object right at the NextObjPtr. Because allocating an object simply means adding a value to a pointer, memory allocation in the managed heap is blazingly fast. Furthermore, objects allocated consecutively sit next to each other in memory, providing excellent locality of reference. This keeps your working set small and ensures that the CPU cache is utilized highly efficiently.- The Marking Phase: The CLR suspends all threads to prevent state changes. It then sets a bit in every object's sync block index to 0 (indicating it should be deleted). Next, the CLR walks through all active roots; if a root points to an object, that object is marked (its bit is set to 1). The GC then recursively marks any objects referenced by fields within that marked object.
- The Compacting Phase: Once the reachable objects are marked, the unmarked objects are considered unreachable garbage. The CLR then shifts the memory consumed by the marked (surviving) objects down in the heap, compacting them so they are contiguous. This compaction restores locality of reference and entirely eliminates address space fragmentation. The
NextObjPtris then reset to point just after the last surviving object, and the application's threads are resumed.
/debug switch, the JIT compiler artificially extends the lifetime of all local variables to the end of the method. This is done to aid in debugging, allowing you to inspect variables at breakpoints without them suddenly disappearing due to a background GC. Be aware that this means a program might work perfectly in a Debug build but fail or behave differently in a Release build if it relied on an object surviving longer than its actual reachable scope.- The newer an object is, the shorter its lifetime will be.
- The older an object is, the longer its lifetime will be.
- Collecting a portion of the heap is faster than collecting the whole heap.
- Generation 0: All newly constructed objects start here. When Gen 0 reaches its assigned memory budget, a GC is triggered. Because new objects usually die young, collecting Gen 0 reclaims a massive amount of memory incredibly quickly (often in less than 1 millisecond).
- Generation 1: Objects that survive a Gen 0 collection are promoted to Gen 1. If Gen 1 reaches its budget, the GC collects both Gen 0 and Gen 1.
- Generation 2: Objects that survive a Gen 1 collection are promoted to Gen 2. This generation contains long-lived objects.
GC.Collect(), when Windows reports low system memory, when an AppDomain unloads, or when the CLR shuts down.- Workstation Mode: Fine-tuned for client-side applications, optimizing for low-latency collections to prevent UI freezing.
- Server Mode: Fine-tuned for server-side applications, assuming it owns all CPU cores. The heap is split into several sections (one per CPU), and the GC runs in parallel across all CPUs to maximize throughput.
GCSettings.GCLatencyMode property. Setting it to LowLatency or SustainedLowLatency tells the GC to heavily avoid performing Gen 2 collections, which is vital for time-sensitive operations like trading applications or animation rendering.Finalize method (represented by the ~ destructor syntax in C#).Finalize method, the CLR adds a pointer to it in the finalization list. When a GC determines the object is garbage, it removes it from the finalization list and moves it to the freachable queue. This action actually resurrects the object, preventing its memory from being reclaimed. A special, high-priority CLR thread then reads from the freachable queue and executes the object's Finalize method. Because the object was resurrected, it requires a second garbage collection for its memory to finally be reclaimed.System.Runtime.InteropServices.SafeHandle class to securely wrap native handles. SafeHandle derives from CriticalFinalizerObject, which guarantees the CLR will call its Finalize method even if an AppDomain is rudely aborted, preventing resource leaks in host environments like SQL Server.IDisposable interface. The Dispose method allows you to deterministically close the resource precisely when you are done with it. C# makes this clean and safe with the using statement, which guarantees Dispose is called via a finally block even if an exception is thrown.IntPtr), but the native resource it wraps is huge (e.g., a 10 MB bitmap). The GC might not collect the object because it only sees 4 bytes of managed memory being consumed, inadvertently causing the system to run out of RAM. To fix this, you can call GC.AddMemoryPressure when the object is created and GC.RemoveMemoryPressure when it is destroyed, forcing the GC to recognize the true footprint of the resource and collect it more aggressively.System.Runtime.InteropServices.GCHandle struct.GCHandleType flags:- Normal: Keeps the object alive. Used to pass a managed object pointer to unmanaged code so that a GC doesn't delete the object while native code is using it.
- Pinned: Keeps the object alive and prevents the GC from moving (compacting) it in memory. Essential when native code is actively writing to a managed memory buffer.
- Weak: Monitors an object's lifetime without keeping it alive. If the GC determines the object is garbage, the handle's reference is set to null.
- WeakTrackResurrection: Similar to
Weak, but it waits to nullify the reference until after the object'sFinalizemethod has run and it has been officially destroyed.
System.Runtime.CompilerServices.ConditionalWeakTable<TKey, TValue> class. This incredible thread-safe class allows you to dynamically attach data to an object, and the CLR guarantees the data will be automatically destroyed the moment the key object is garbage collected.IDisposable and SafeHandle, you can build highly optimized, memory-efficient .NET applications that easily scale without succumbing to memory leaks or latency spikes!CoCreateInstance. Instead, the host calls the CLRCreateInstance function, which is implemented in a special file called MSCorEE.dll (often affectionately referred to as the shim). The shim's primary job is to evaluate the environment and determine exactly which version of the CLR to load into the host's process.CLRCreateInstance is called, it returns an ICLRMetaHost interface to the host application. The host can then call GetRuntime to request a specific version of the CLR.- Add-ins can be written in any programming language.
- Code is Just-In-Time (JIT) compiled for native speed.
- Memory leaks and corruption are avoided via Garbage Collection.
- Code runs in a highly secure, heavily monitored sandbox.
- Strict Object Isolation: Objects created in one AppDomain cannot be accessed directly by code in another AppDomain. This enforces a clean boundary, guaranteeing that code in AppDomain A cannot easily corrupt data in AppDomain B.
- Clean Unloading: While the CLR does not allow you to unload a single assembly from memory, it does allow you to completely unload an AppDomain. This takes all the assemblies loaded inside that AppDomain down with it.
- Individual Security: Every AppDomain can have its own permission set. You can run your host code with full trust, but load a third-party add-in into an AppDomain restricted from accessing the file system or network.
- Individual Configuration: Each AppDomain can have its own configuration settings, altering how the CLR searches for assemblies, handles binding redirects, or manages shadow copying.
MSCorLib.dll) as Domain-Neutral Assemblies. A domain-neutral assembly shares its compiled code and type objects across all AppDomains in the process, but the catch is that a domain-neutral assembly can never be unloaded until the entire process terminates.- Marshal-by-Reference: If an object’s type derives from
System.MarshalByRefObject, the CLR creates a proxy type in the destination AppDomain. This proxy object looks exactly like the real object, but its internal fields actually maintain a handle pointing to the real object in the source AppDomain. When you call a method on the proxy, the calling thread literally transitions synchronously across the AppDomain boundary, executes the code in the original AppDomain under its specific security context, and returns the result. (Warning: Accessing instance fields on a proxy object is remarkably slow—up to 6 times slower—because the CLR uses reflection behind the scenes to access the data!) - Marshal-by-Value: If an object is not derived from
MarshalByRefObjectbut is decorated with the[Serializable]attribute, the CLR serializes the object into a byte array, moves the bytes across the boundary, and deserializes them into a perfect, independent clone in the new AppDomain. No proxy is used; the two objects live entirely separate lives. - Non-Marshalable Types: If a type fits neither of these categories, the CLR strictly forbids it from crossing the boundary. Attempting to do so will result in a fatal
SerializationException.
AppDomain.Unload(), the CLR executes a carefully choreographed shutdown sequence:- Suspension: The CLR pauses all threads in the process that have ever run managed code.
- Thread Aborting: The CLR examines thread stacks. If a thread is currently executing code inside the target AppDomain, the CLR forces it to throw a
ThreadAbortException. This forces the thread to unwind and execute itsfinallyblocks for proper cleanup. (Note: The CLR will delay the abort if the thread is currently inside a finally block, catch block, unmanaged code, or Constrained Execution Region to prevent unpredictable state corruption.) - Proxy Severing: The CLR marks any proxy objects pointing to the dying AppDomain as invalid. Future attempts to use these proxies will throw an
AppDomainUnloadedException. - Garbage Collection: The CLR triggers a garbage collection to execute
Finalizemethods and reclaim the memory of the objects native to that AppDomain. - Resumption: All remaining threads are allowed to continue running.
Unload call gives up and throws a CannotUnloadAppDomainException.AppDomain.MonitoringIsEnabled = true (a one-way switch that cannot be turned off), the host gains access to vital telemetry.MonitoringTotalProcessorTime: The total CPU time the AppDomain has consumed.MonitoringTotalAllocatedMemorySize: The total bytes allocated by the AppDomain over its lifetime.MonitoringSurvivedMemorySize: The bytes currently in use by the AppDomain (accurate as of the last GC).MonitoringSurvivedProcessMemorySize: The bytes currently in use by the entire CLR instance.
FirstChanceException event.catch blocks. The callback receives the notification but is strictly an observer—it cannot handle or swallow the exception.FirstChanceException event in the caller's AppDomain, continuing all the way up the stack until the process is ultimately terminated by the OS if no handler is found.- Executable Applications (Console, WPF, Windows Forms): The OS loads the shim, examines the EXE's CLR header, and loads the appropriate CLR. The CLR creates the default AppDomain, runs the
Mainmethod, and tears down the AppDomain when the application exits. - Silverlight Rich Internet Applications: Silverlight runs a specialized CLR (
CoreClr.dll) inside the browser. Each Silverlight control on a webpage runs in its own highly-restricted AppDomain sandbox. Navigating away unloads the AppDomain instantly. - ASP.NET and XML Web Services: ASP.NET is implemented as an ISAPI DLL. When a request arrives, ASP.NET creates an AppDomain based on the virtual root directory and loads the web application's assemblies into it. Multiple web applications can run safely inside a single Windows worker process. Furthermore, ASP.NET uses an AppDomain feature called shadow copying; if you update a DLL file on the server, ASP.NET detects the change, gracefully unloads the old AppDomain, and spins up a new one dynamically without dropping the server process!
- SQL Server: Because SQL Server allows developers to write stored procedures in C#, it utilizes AppDomains to securely sandbox that code, ensuring a rogue SQL query can't crash the database engine.
- Your Own Imagination: You can build word processors or spreadsheets that allow users to write macros in C#. By compiling these macros and tossing them into a secured AppDomain, you provide massive extensibility without sacrificing application stability.
System.AppDomainManager. This class must be installed in the Global Assembly Cache (GAC) because it requires absolute full trust.AppDomainManager. Once active, your manager object gets a say in every new AppDomain created in the process. It can intercept creations, alter security settings, or even outright reject an add-in's attempt to spin up a new AppDomain.- Graceful Thread Abort: Throws a
ThreadAbortException, allowingfinallyblocks to run. - Rude Thread Abort: If the thread doesn't die quickly, the CLR brutally kills it, bypassing
finallyblocks. - Graceful/Rude AppDomain Unload: Rips the entire AppDomain out of memory.
- Disable CLR / Terminate Process: Complete nuclear option.
Monitor.Enter), a simple thread abort is too dangerous. The lock would be orphaned, and shared data might be corrupted. In this scenario, the CLR's escalation policy automatically bypasses the thread abort and immediately initiates an AppDomain Unload to violently purge the corrupted state and protect the rest of the process.Thread.Abort(). The thread unwinds, blowing past the untrusted code until it crosses back over the AppDomain boundary into the host's trusted code. Here, the host catches the ThreadAbortException and calls Thread.ResetAbort(). This brilliant method tells the CLR to stop re-throwing the exception, effectively "curing" the thread and allowing the host to safely return it to the thread pool for the next client request!TypeRef and AssemblyRef metadata tables, the JIT compiler determines exactly which assembly defines the required type. It grabs the identity components—name, version, culture, and public key token—and attempts to load the matching assembly into the current AppDomain.- Assembly.Load: This is the primary method to load an assembly. When invoked, it applies version-binding redirection policies and searches the Global Assembly Cache (GAC), followed by the application's base directory, private paths, and codebase locations. If it fails to find the assembly, it throws a
FileNotFoundException. You can also specify aProcessorArchitecture(such as MSIL, x86, IA64, AMD64, or Arm) to force the CLR to load a CPU-specific version of the assembly. - Assembly.LoadFrom: This method allows you to pass a specific file path or URL. Internally, it extracts the
AssemblyDefmetadata usingAssemblyName.GetAssemblyNameand then callsAssembly.Load. If it passes a URL, the CLR automatically downloads the file to the user's cache and loads it from there. - What to Avoid: AppDomain.Load: Managed code developers should generally avoid calling
AppDomain.Load. This method is designed for unmanaged hosts to inject assemblies. It applies the calling AppDomain's policies and paths, not the specified AppDomain's settings, and then marshals the assembly by value back to the caller—which often results in an unexpectedFileNotFoundException.
Assembly.ReflectionOnlyLoadFrom or Assembly.ReflectionOnlyLoad. Because these methods skip standard binding, you must register a callback with the AppDomain.ReflectionOnlyAssemblyResolve event to manually load any referenced assemblies your analysis encounters.AppDomain.ResolveAssembly event, your code can extract the embedded DLL byte stream at runtime and load it using Assembly.Load(Byte[]). Keep in mind that this technique does increase your application's memory footprint.System.Reflection namespace offers an object model over these metadata tables, allowing you to parse them at runtime.- Loss of Compile-Time Type Safety: Because reflection heavily relies on string identifiers (e.g., asking for
"int"instead of"System.Int32"), the compiler cannot verify the type, meaning errors will only surface as runtime exceptions ornullreturns. - Slow Execution: Searching through metadata using strings is extremely slow.
Assembly.ExportedTypes, which returns all publicly exported types.- System.Type: Represents a lightweight type reference. The CLR ensures there is only one
Typeobject per type in an AppDomain, so you can safely use equality operators (==) to compare them. - System.TypeInfo: Represents a deep type definition. Obtaining a
TypeInfoobject forces the CLR to resolve and load the assembly defining the type, which is an expensive operation.
GetTypeInfo() extension method (to get a TypeInfo from a Type) and the AsType() method (to go back). Once you have a TypeInfo object, you can query properties like IsPublic, IsSealed, IsValueType, and BaseType.Type object, you will likely want to instantiate it. The FCL offers several mechanisms:- System.Activator.CreateInstance: The simplest method. You pass a
Typeobject and constructor arguments, and it returns a reference to the new object. Overloads taking a string representing the type return aSystem.Runtime.Remoting.ObjectHandle, which must be materialized by callingUnwrap(). - System.Activator.CreateInstanceFrom: Similar to
CreateInstance, but requires string parameters for the type and assembly, loads the assembly viaLoadFrom, and always returns anObjectHandle. - System.AppDomain Methods: Methods like
CreateInstanceAndUnwrapallow you to construct a type inside a specific AppDomain instead of the calling AppDomain. - ConstructorInfo.Invoke: Using a
TypeInfoobject, you can isolate a specific constructor and invoke it directly. The object is created in the calling AppDomain.
- The Host SDK Assembly: Define the communication contracts (interfaces or base classes) here. You must give this assembly a strong name and strictly avoid making breaking changes to it.
- The Add-In Assembly: Add-in developers reference your Host SDK assembly and implement the interfaces. They can update their add-in independently of the host.
- The Host Application Assembly: This references the Host SDK assembly and uses reflection to discover and load the Add-In assemblies.
MarshalByRefObject, the host can instantiate types in the add-in AppDomain and communicate across the AppDomain boundary securely.System.Reflection.MemberInfo, which encapsulates common properties like Name, DeclaringType, Module, and CustomAttributes.MemberInfo, the hierarchy branches into concrete classes: TypeInfo, FieldInfo, MethodBase (which branches into ConstructorInfo and MethodInfo), PropertyInfo, and EventInfo. You can query a type's members by calling TypeInfo.DeclaredMembers or grab specific members using GetDeclaredField, GetDeclaredMethod, etc.. For methods, you can call GetParameters to obtain a ParameterInfo array outlining the expected arguments.MemberInfo derived object, you invoke it based on its type:- FieldInfo: Call
GetValueorSetValue. - ConstructorInfo: Call
Invoketo construct an instance. - MethodInfo: Call
Invoketo execute the method. - PropertyInfo: Call
GetValue(for the get accessor) orSetValue(for the set accessor). - EventInfo: Call
AddEventHandlerorRemoveEventHandler.
Type and MemberInfo objects consumes a massive amount of managed memory. If you are building a tool that needs to cache this information, you can drastically reduce memory consumption by converting these heavyweight objects into lightweight value types known as runtime handles (RuntimeTypeHandle, RuntimeFieldHandle, and RuntimeMethodHandle)..TypeHandle, .FieldHandle, or .MethodHandle properties on the respective reflection objects. When you are ready to invoke the member later, you convert the handle back into a reflection object using static methods like Type.GetTypeFromHandle, FieldInfo.GetFieldFromHandle, or MethodBase.GetMethodFromHandleSystem.Runtime.Serialization.IFormatter interface—that do all the heavy lifting. The most common one is the BinaryFormatter (the SoapFormatter is considered obsolete as of .NET 3.5 and should be avoided in production),.Stream (like a MemoryStream or FileStream), instantiate a BinaryFormatter, and call the formatter's Serialize method, passing in the stream and the root object of your graph,.Deserialize method, passing in the stream. The formatter extracts the bytes, instantiates the objects, and initializes all their fields to the exact state they were in when serialized. Pro tip: You can use this exact mechanism to perform a deep copy (or clone) of an object by serializing it to a MemoryStream and immediately deserializing it back out,.Assembly.Load to load that specific assembly back into the AppDomain. If your application originally loaded the assembly using Assembly.LoadFrom, the deserialization process might fail to find the file and throw a SerializationException. If you are doing this, you'll need to register a callback with the AppDomain.AssemblyResolve event to manually assist the CLR in finding the assembly file using Assembly.LoadFrom.SerializationException.[Serializable] custom attribute to your class or struct. When serializing a graph, the formatter verifies that every single object in the graph has this attribute. Because formatters don't pre-validate the entire graph before writing to the stream, encountering a non-serializable object halfway through will throw an exception and leave you with a corrupted stream. (To mitigate this, you can serialize to a MemoryStream first, and only write it to disk or the network if it completes successfully).[Serializable] attribute applied. If the base class omits it, it cannot be serialized, because the base class fields are fundamentally part of the derived object.[Serializable] attribute.[Serializable], the formatter serializes every single instance field by default. However, you might have fields that should not be serialized. Two common reasons include:- The field holds a Windows kernel handle (like a file or thread handle) which would be completely meaningless when deserialized in another process or machine.
- The field holds calculated data (like the
Areaof aCirclederived from itsRadius). Omitting calculated fields shrinks the serialized payload and boosts performance.
[NonSerialized] attribute to it.Area field of a Circle, when the object is deserialized, that field will be initialized to 0, leaving your object in a corrupted state. To fix this, the .NET Framework provides four special method attributes: [OnSerializing], [OnSerialized], [OnDeserializing], and [OnDeserialized],.StreamingContext parameter and decorating it with the [OnDeserialized] attribute, the formatter will automatically invoke your method after all fields have been deserialized,. This is the perfect place to recalculate your Area field.[OnDeserialized]. During deserialization, the formatter tracks all objects requiring this callback and invokes them in reverse order. This ensures that inner, contained objects finish their deserialization logic before the outer objects that hold them are initialized. A prime example of this is the Dictionary class, which waits for its items to fully deserialize and calculate their hash codes before it places them into its internal hash buckets.SerializationException. To prevent this and make your types version-resilient, you can apply the [OptionalField] attribute to any newly added fields.System.Runtime.Serialization.FormatterServices type. This static class does the heavy lifting for the formatters.- The formatter calls
FormatterServices.GetSerializableMembers, which uses reflection to grab all instance fields (ignoring those marked[NonSerialized]),. - It passes the object and the member list to
FormatterServices.GetObjectData, which returns a parallel array of the actual values held in those fields. - The formatter writes the assembly's identity and the type's full name to the stream.
- It iterates over the arrays, writing the member names and values to the stream.
- The formatter reads the assembly identity and type name, loading the assembly if necessary.
- It calls
FormatterServices.GetTypeFromAssemblyto get the exactSystem.Type,. - It calls
FormatterServices.GetUninitializedObject, which allocates memory for the object but critically does not call a constructor, leaving all bytes zeroed out. - It calls
GetSerializableMembersto figure out which fields need to be populated. - It extracts the values from the stream into an object array.
- Finally, it calls
FormatterServices.PopulateObjectMembers, which dynamically injects the values directly into the uninitialized object's fields.
[NonSerialized], [OnDeserialized], etc.) are fantastic, sometimes you need absolute, granular control over the data being serialized, or you want to avoid the performance overhead of the formatter's reflection-based field extraction. To achieve this, your type can implement the ISerializable interface.GetObjectData(SerializationInfo info, StreamingContext context). However, you must also implement a special constructor (usually marked private or protected for security) with the exact same signature: YourType(SerializationInfo info, StreamingContext context),. Because these methods handle raw data, you should secure them by applying the [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] attribute.ISerializable object, it calls your GetObjectData method. Inside, you explicitly call AddValue on the SerializationInfo object for every piece of data you want to persist. During deserialization, the formatter calls your special constructor, and you pull the data back out using methods like GetInt32, GetString, or GetValue.ISerializable is powerful, but it comes with a major caveat. Once a class implements it, all derived classes must implement it as well, and they must remember to call the base class's GetObjectData method and special constructor. If they don't, the base class fields won't serialize! Even worse, if your class inherits from a base class that does not implement ISerializable, you are entirely responsible for manually grabbing the base class's fields using FormatterServices.GetSerializableMembers and injecting them into the SerializationInfo bag (often prefixing them with the base class's name to avoid collisions),,,.[OnDeserialized], etc.) whenever possible. Only fall back to ISerializable when absolutely necessary.StreamingContext structure is passed into GetObjectData and the [OnSerializing] family of methods. It contains a State property (a bit-flag from StreamingContextStates) that indicates the destination or source, such as CrossProcess, CrossMachine, File, Remoting, or Clone,,.All. You can change this by constructing a new StreamingContext and assigning it to the formatter.Context property before executing Serialize or Deserialize,.- Singletons: Types like
DBNullare designed to have only one instance per AppDomain. Deserializing aDBNullobject shouldn't create a new instance; it should resolve to the existing one. - Reflection Types: A
TypeorMemberInfoobject has only one instance per specific member in the AppDomain. If you serialize an array of five references to aMemberInfo, they should deserialize back into references to the AppDomain's actualMemberInfoobject. - Remoting: A server object serializes data that deserializes into a proxy object on the client side.
ISerializable and its GetObjectData method serializes information representing a helper class instead of itself. During deserialization, the formatter constructs the helper class. This helper class implements the System.Runtime.Serialization.IObjectReference interface. The formatter automatically calls its GetRealObject(StreamingContext) method, which returns the actual object (e.g., the existing Singleton) that the caller really wanted. The temporary helper class is then immediately garbage collected.[Serializable] attribute on it? Or what if you want to intercept the serialization of an older type and seamlessly map its fields to a newer version?,.ISerializationSurrogate, which requires GetObjectData and SetObjectData methods. Then, you register your surrogate with a SurrogateSelector object, specifying exactly which type your surrogate is responsible for. Finally, you assign the selector to the formatter.SurrogateSelector property.GetObjectData. On the way back in, it creates an uninitialized instance of the object and hands it to your surrogate's SetObjectData to populate the fields.SurrogateSelector objects together using the ISurrogateSelector.ChainSelector method, allowing you to layer different surrogates for remoting, version mapping, and custom type handling,.Version1Type directly into an instance of Version2Type?,.System.Runtime.Serialization.SerializationBinder and override its BindToType method,. You attach this object to the formatter.Binder property before calling Deserialize.BindToType method, passing it the assembly name and type name it just read from the byte stream. Inside this method, you can execute whatever string-mapping logic you want, and return the exact System.Type that the formatter should actually construct. This gives you the ultimate power to reshape your application's data structures across version updates without abandoning your legacy serialized data!- File Names and Namespaces: The name of a
.winmdfile must perfectly match the namespace containing the WinRT components (or be a parent namespace). The Windows file system is case-insensitive, so namespaces differing only by case are strictly forbidden. - Classes: While WinRT supports inheritance and polymorphism, almost no WinRT components actually use them (aside from XAML UI components). This is to cater to languages like JavaScript that do not natively support class inheritance. Furthermore, WinRT classes cannot expose public fields.
- Structures: WinRT supports structures (value types), but they can only contain public fields of core data types or other WinRT structures. They cannot contain constructors or helper methods. For convenience, the CLR projects several WinRT structures (like
Point,Rect,Size, andTimeSpanin theWindows.Foundationnamespace) into their native CLR equivalents, restoring the constructors and methods you expect. - Nullable Structures: The CLR implicitly projects the WinRT
Windows.Foundation.IReference<T>interface into the familiar .NETSystem.Nullable<T>type. - Enumerations: WinRT enums are backed by 32-bit integers (either signed
intfor discrete values or unsigneduintfor combinable bit flags). - Events: Because most WinRT components are sealed (no inheritance), WinRT uses a
TypedEventHandler<TSender, TResult>delegate where the sender is strongly typed rather than just a genericSystem.Object. The CLR also projectsWindows.Foundation.EventHandler<T>directly into .NET'sSystem.EventHandler<T>. - Exceptions: WinRT components use COM
HRESULTvalues to indicate failure. The CLR catches these and projects them as standard .NET Exception objects. For example,E_OUTOFMEMORYbecomesSystem.OutOfMemoryException. - Collections: The CLR team did an enormous amount of work to project WinRT collection interfaces into standard .NET generic collections. For example, WinRT's
IIterable<T>becomesIEnumerable<T>,IVector<T>becomesIList<T>, andIMap<K, V>becomesIDictionary<TKey, TValue>. This makes passing data between WinRT and .NET completely seamless.
- Asynchronous programming.
- Interoperating between WinRT streams and .NET streams.
- Passing raw blocks of data between the CLR and WinRT.
IAsyncInfo:IAsyncAction: Completes with no return value.IAsyncOperation<TResult>: Completes and returns a value.IAsyncActionWithProgress<TProgress>: No return value, but provides periodic progress updates.IAsyncOperationWithProgress<TResult, TProgress>: Returns a value and provides periodic progress updates.
Completed properties of these interfaces. Instead, you want to use the await keyword. But how does C# await a WinRT interface?System.Runtime.WindowsRuntime.dll called GetAwaiter. When you write await KnownFolders.MusicLibrary.GetFileAsync("Song.mp3"), the C# compiler automatically calls the GetAwaiter extension method on the returned IAsyncOperation<StorageFile>. Internally, this adapter constructs a TaskCompletionSource, registers a callback with the WinRT operation, and returns a TaskAwaiter that integrates perfectly into the C# state machine.GetAwaiter extension method isn't enough. Instead, you must explicitly call the AsTask extension method.AsTask method converts the WinRT IAsyncXxx interface into a standard .NET Task or Task<TResult>. You can pass a CancellationToken into AsTask to wire up cancellation, and you can pass an IProgress<TProgress> object (like the Progress<T> class) to receive the progress updates.IRandomAccessStream, IInputStream, or IOutputStream. Naturally, if you want to parse that file using a .NET API (like XElement.Load()), you need a standard .NET System.IO.Stream.System.IO.WindowsRuntimeStreamExtensions class offers a suite of extension methods: AsStream(), AsStreamForRead(), AsStreamForWrite(), AsInputStream(), and AsOutputStream().AsStreamForRead() on a WinRT stream, the framework projection doesn't just cast the object; it actually wraps it in an adapter and implicitly creates a buffer in the managed heap. By default, this buffer is 16 KB.0 for the buffer size.Windows.Storage.Streams.IBuffer interface. If you have a .NET Byte[] array and need to pass it to a WinRT API expecting an IBuffer, you can use the AsBuffer() extension method. Conversely, if you receive an IBuffer from WinRT, you can call the ToArray() extension method to extract a .NET Byte[], or AsStream() to read the buffer natively. Under the hood, the .NET Framework provides a System.Runtime.InteropServices.WindowsRuntimeBuffer class to wrap managed arrays into the required native structure./t:winmdobj switch. This switch alters how certain IL (like events) is emitted to be compatible with WinRT..winmdobj file, a utility called WinMDExp.exe (WinMD export) kicks in. WinMDExp.exe aggressively analyzes your metadata to ensure you haven't violated any WinRT type system rules (e.g., making sure you have no public fields, and ensuring your methods don't use unsupported types). It then massages the metadata, translating .NET types into their WinRT equivalents (e.g., converting your IList<String> signatures into IVector<String>), and spits out a final .winmd file.- Thread Kernel Object: A data structure managed by the OS that contains thread properties and the "thread context". The context is a memory block storing the CPU's registers, which consumes about 700 bytes on x86, 1,240 bytes on x64, and 350 bytes on ARM.
- Thread Environment Block (TEB): A 1-page (4 KB) block of user-mode memory that stores the head of the exception-handling chain, thread-local storage data, and graphics data structures for GDI and OpenGL.
- User-Mode Stack: Used to store local variables and method arguments. By default, Windows reserves 1 Megabyte of memory for every thread's user-mode stack.
- Kernel-Mode Stack: When your application calls an OS kernel function, Windows copies the arguments from the user-mode stack to the kernel-mode stack for security validation. This consumes 12 KB on 32-bit Windows and 24 KB on 64-bit Windows.
- DLL Thread-Attach and Thread-Detach Notifications: Whenever a thread is created or destroyed, Windows calls the
DllMainfunction of every unmanaged DLL loaded in the process. If your process has 400 DLLs loaded (like Visual Studio often does), 400 functions must execute before your new thread can even begin doing the work you created it to do!
- Hyperthreaded Chips: This Intel technology allows a single physical chip to look like two chips to the OS. It duplicates architectural states (like registers) but shares execution resources.
- Multi-Core Chips: Modern processors pack multiple CPU cores (2, 4, 8, or more) onto a single chip. We even have multi-core chips in our mobile phones today. To scale software moving forward, developers must embrace threading to take advantage of these multi-core architectures.
System.Threading.Thread class from the API because it encouraged terrible programming practices. You can no longer explicitly create a thread, put it to sleep, or suspend it in Windows Store apps.- You need the thread to run at a non-normal priority (Thread Pool threads always run at normal priority).
- You need a "foreground" thread to prevent the application from terminating until the thread's task is complete.
- The task is extremely long-running, and you don't want to tax the thread pool's scaling logic.
- You specifically need to be able to forcefully abort the thread prematurely using
Thread.Abort.
System.Threading.Thread, passing a method matching the ParameterizedThreadStart delegate into its constructor. You then call the Start method, passing in the state object you want the thread to process. You can also force the calling thread to wait for the dedicated thread to finish by calling the Join method.- Responsiveness: In client-side GUI applications, offloading work to another thread keeps the main GUI thread unblocked, ensuring the application remains responsive to user clicks and keystrokes.
- Performance: If you are running on a machine with multiple CPU cores, Windows can schedule multiple threads concurrently, vastly improving your application's throughput by performing tasks in parallel.
- Process Priority Class: You assign your application a priority class (Idle, Below Normal, Normal, Above Normal, High, Realtime). Normal is the default.
- Relative Thread Priority: Within your process, you assign your threads a relative priority (Idle, Lowest, Below Normal, Normal, Above Normal, Highest, Time-Critical).
- You should almost never alter your Process Priority Class. Doing so affects every thread in your app and can disrupt the whole OS.
- When altering a thread's priority, it is much better to lower a thread's priority (for long-running compute tasks) rather than raise one.
- If you must raise a thread's priority, that thread should spend 99% of its life in a sleeping/waiting state (like waiting for a keystroke) so it does not starve the rest of the OS.
finally blocks in the background threads do not execute.- Foreground Threads (Default): The primary thread of your application, and any thread you explicitly create via
new Thread(), defaults to being a foreground thread. Use these only for mission-critical tasks (like flushing data to disk) where you absolutely cannot allow the app to close until the work is done. - Background Threads: Thread Pool threads, and any unmanaged native threads that enter the CLR, default to being background threads. Use these for non-critical tasks (like background spell checking) that can safely be interrupted if the user decides to close the application.
Thread.IsBackground property to true or false. Caution: Be very careful with foreground threads! A common bug is accidentally creating a foreground thread that stays alive, causing your application process to hang in the background indefinitely even after the user closes the main UI window.ThreadPool.QueueUserWorkItem. This method requires you to pass a callback method whose signature matches the WaitCallback delegate, which takes a single Object parameter and returns void.ThreadPool.QueueUserWorkItem(ComputeBoundOp, 5);
ComputeBoundOp method. Because the work is performed asynchronously, the thread that queued the work continues executing immediately, allowing multiple operations to run concurrently across different CPUs.ExecutionContext class:ExecutionContext.SuppressFlow();
ThreadPool.QueueUserWorkItem(SomeMethod);
ExecutionContext.RestoreFlow();
- CancellationTokenSource: An object created by the code that wants to control the cancellation.
- CancellationToken: A lightweight value type (struct) handed to the asynchronous operation, allowing it to check if it has been canceled.
CancellationToken.IsCancellationRequested property and exit gracefully if it returns true. Alternatively, you can simply call token.ThrowIfCancellationRequested(), which automatically throws an OperationCanceledException if the token has been canceled.CancellationToken also allows you to register callback methods (using Register) that execute the moment the token is canceled. You can even link multiple tokens together using CancellationTokenSource.CreateLinkedTokenSource, creating a master token that cancels if any of the linked sources are canceled. For operations that need to timeout, you can instantiate a CancellationTokenSource with a delay, or call its CancelAfter method to force a self-cancellation after a set time.ThreadPool.QueueUserWorkItem is lightweight, it is severely limited. It offers no built-in way to know when the operation finishes, no way to capture a return value, and clumsy exception handling. To solve these limitations, Microsoft introduced Tasks.Task.Run and passing it an Action or Func<TResult> delegate.Task<Int32> t = Task.Run(() => Sum(1000000000));
Task<TResult>, you can query its Result property. Warning: Querying the Result property or calling the Wait method will block the calling thread until the task completes. If you need to wait on multiple tasks, the Task class provides WaitAll (blocks until all tasks complete) and WaitAny (blocks until at least one task completes).Task object. Later, when you call Wait() or query the Result property, the framework throws an AggregateException. This exception encapsulates a collection of exceptions (because a parent task could have multiple child tasks that failed simultaneously). You can process these by querying the InnerExceptions property, or use the Flatten and Handle methods to drill down into the root causes.Wait(), you should schedule a "continuation" task that executes automatically when the antecedent task completes using ContinueWith.Task<Int32> t = Task.Run(() => Sum(10000));
t.ContinueWith(task => Console.WriteLine("The sum is: " + task.Result));
TaskContinuationOptions, you can orchestrate complex workflows. You can tell a continuation to run OnlyOnRanToCompletion, OnlyOnFaulted, or OnlyOnCanceled.TaskCreationOptions.AttachedToParent. When this is done, the parent task will not transition into a completed state until all of its attached children have finished executing.TaskFactory or TaskFactory<TResult>.TaskScheduler queues tasks to the CLR thread pool. However, the framework also provides a synchronization context task scheduler which routes tasks specifically to a GUI thread (useful for updating Windows Forms or WPF components without throwing cross-thread exceptions).System.Threading.Tasks.Parallel class.for loop that executes sequentially on one thread:for (Int32 i = 0; i < 1000; i++) DoWork(i);
Parallel.For(0, 1000, i => DoWork(i));
Parallel class also provides Parallel.ForEach for iterating over collections, and Parallel.Invoke for executing several distinct methods concurrently.- These methods are blocking. The thread that calls
Parallel.Forparticipates in the work but will suspend itself until all thread pool threads finish their portions. - You can break out of a loop early by accessing the
ParallelLoopStateobject passed to your loop body delegate. CallingStop()aborts the loop entirely, whileBreak()ensures that all iterations prior to the current index complete before exiting. - If your items require thread-local state (to avoid locking shared variables), you can use overloads that accept
localInit,body, andlocalFinallydelegates, allowing each thread to safely accumulate a local result before merging it into a master total at the very end.
.AsParallel() extension method on the source collection.var query = from type in assembly.GetExportedTypes().AsParallel() ...
.AsOrdered(), though this carries a performance penalty.foreach loop forces a single thread to iterate through the processed data. If you want the items processed in parallel as they emerge from the pipeline, use PLINQ’s .ForAll() method. Finally, you can exert precise control over PLINQ's execution by utilizing methods like WithCancellation, WithDegreeOfParallelism (to limit how many cores are used), and WithMergeOptions (to balance memory consumption versus speed when buffering results).System.Threading.Timer class.Timer, you pass a TimerCallback delegate, a state object, a dueTime (when it should first execute), and a period (how often it repeats). The magic of the Timer is that it doesn't tie up a thread while waiting. When the timer expires, the thread pool injects a work item into its queue, and a thread pool thread handles it.Timer is letting the variable holding the Timer reference go out of scope. If it does, the Garbage Collector will reclaim the object and your periodic operation will mysteriously stop executing. You must ensure the Timer object is kept alive by a variable.async/await, you can easily replace Timer entirely by placing a Task.Delay(ms) inside an asynchronous while(true) loop. Because await yields the thread, this accomplishes periodic execution without blocking any threads.ThreadPool.SetMaxThreads usually degrades performance and can easily trigger thread starvation or deadlocks.- When a non-worker thread (like your application's
Mainthread) queues work, or when you use aTimer, the item goes into the Global Queue. Because multiple threads access the global queue, it uses a thread synchronization lock. Items here are processed First-In-First-Out (FIFO). - When a thread pool worker thread schedules a
Task(such as a task spawning child tasks), the item is placed in that specific worker thread's Local Queue.
FileStream.Read, your thread transitions from user-mode managed code into the Windows kernel. Windows allocates an I/O Request Packet (IRP), initializes it with the file handle and buffer details, and queues it to the hardware device driver.FileOptions.Asynchronous flag and call ReadAsync, Windows still allocates an IRP and queues it to the hardware driver, but your thread returns immediately. Your thread does not block; it returns to the thread pool to handle other client requests.Task object, allowing your code to resume and process the data.- Resource Efficiency: A single thread can handle thousands of client requests and database responses without blocking.
- Zero Context Switching: Because threads don't block, Windows doesn't need to context-switch, keeping your CPUs running at maximum speed.
- Concurrent Execution: If you need to download 10 images that take 5 seconds each, doing it synchronously takes 50 seconds. With asynchronous I/O, all 10 downloads happen concurrently in the background, finishing in exactly 5 seconds!
async and await keywords.async keyword, you are allowed to use the await operator inside it. The await operator tells the compiler to asynchronously wait for a Task to complete without blocking the current thread. The thread simply returns to the caller, and when the I/O operation finishes, a thread pool thread automatically resumes your method right where it left off.- Your application’s
Mainmethod, constructors, and property/event accessors cannot be markedasync. - You cannot use
reforoutparameters. - You cannot use
awaitinside acatch,finally,unsafe, orlockblock.
async, the C# compiler fundamentally rewrites your code into an IAsyncStateMachine structure.m_state) and hoists all of your method's local variables into fields of the structure. When your code hits an await operator, the compiler extracts an "awaiter" from the Task (by calling GetAwaiter()).goto statement generated by the compiler) right back to the line of code immediately following the await. It is an incredible piece of compiler magic that saves you from writing spaghetti callback code!await operator is that it is heavily extensible. The compiler doesn't strictly require you to await a Task; you can await any object as long as it exposes a GetAwaiter method.Task is the universal wrapper for asynchronous operations, you can build rich combinators. For instance, Richter demonstrates building a TaskLogger class that intercepts and logs pending asynchronous operations. You can also write custom awaiters, like an EventAwaiter, which allows a state machine to suspend execution and resume only when a specific .NET event is raised!async function should return a Task or Task<TResult> so that the caller can track its completion. However, there is one major exception: C# allows you to define an async function with a void return type.void return type, marking them async void allows you to use await inside the handler without breaking the event delegate signature.Async suffix.System.IO.StreamoffersReadAsync,WriteAsync,FlushAsync, andCopyToAsync.HttpClientoffersGetAsyncandPostAsync.SqlCommandoffersExecuteReaderAsyncandExecuteNonQueryAsync.
BeginXxx/EndXxx pattern (the IAsyncResult model), you can easily modernize them. By using TaskScheduler.FromAsync, you can wrap legacy operations into a Task that you can elegantly await. For legacy event-based asynchronous patterns (like WebClient), you can wrap the event in a TaskCompletionSource<T> to achieve the exact same thing.Task object with an exception.Task's result directly wraps any exceptions in an AggregateException. However, to make the programming model feel natural, the await operator intentionally unwraps the AggregateException and throws the first inner exception. This allows you to wrap your await calls in standard try/catch blocks exactly as you would with synchronous code.Task API also allows for powerful concurrent execution using Task.WhenAll (which creates a task that completes when a collection of tasks finishes) and Task.WhenAny (which completes as soon as the first task in a collection finishes).async code and these threading models, the FCL uses the System.Threading.SynchronizationContext class. When you await a task, the compiler captures the calling thread's SynchronizationContext. When the I/O finishes, the state machine resumes execution on that captured context. This means if you await a network call on a GUI thread, the code after the await automatically runs on the GUI thread, allowing you to safely update UI controls!.Result on the returned Task, it will deadlock. The async method tries to post the completion back to the GUI thread via the SynchronizationContext, but the GUI thread is frozen waiting for the Task to finish.await operator to ignore the SynchronizationContext by calling .ConfigureAwait(false) on the Task. This allows the continuation to run on a random thread pool thread, avoiding deadlocks and significantly boosting performance.- ASP.NET Web Forms: Set
Async="true"in your page directive. - ASP.NET MVC: Derive from
AsyncControllerand return aTask<ActionResult>. - WCF Services: Implement your service interface as an
asyncfunction returning aTask.
CancellationTokenSource and CancellationToken pattern. Many XxxAsync methods in the FCL accept a CancellationToken. You can pass this token in, and if the user wants to abort the operation, or if you want to apply a timeout (e.g., new CancellationTokenSource(5000)), calling Cancel() on the source will elegantly abort the pending I/O operation and throw an OperationCanceledException.CreateFile (which FileStream calls under the hood), simply do not have an asynchronous equivalent. If you try to open a file on a slow network share, your thread will block.CancelSynchronousIO, which allows one thread to forcibly cancel a synchronous I/O operation blocking another thread. The FCL does not expose this natively, but you can access it via P/Invoke.FileStream, you absolutely must specify the FileOptions.Asynchronous flag if you intend to perform asynchronous I/O.ReadAsync, the CLR will fake the asynchronous behavior by delegating the synchronous read to a thread pool thread. This completely defeats the purpose of asynchronous I/O because it wastes a thread pool thread by forcing it to block. Conversely, if you specify the flag, you must use ReadAsync to get true hardware-level asynchronous performance. (Note: Always avoid File.Create or File.Open if you want async behavior, as they internally omit the Asynchronous flag).SetThreadPriority function (passing the ThreadBackgroundMode flag) to explicitly tell the Windows kernel to process a thread's I/O requests at a low priority, keeping the rest of the system highly responsive.System.Console has internal locking mechanisms to ensure that multiple threads calling Console.WriteLine simultaneously don't output garbled text.CancellationTokenSource.Cancel). In general, to avoid the need for locks, avoid static fields, favor value types (which are copied by value), and keep concurrent data access strictly read-only.- User-Mode Constructs: These use special CPU instructions to coordinate threads in hardware, making them blazingly fast. The major downside is that the Windows operating system is entirely unaware that a thread is blocked on a user-mode construct. If a thread cannot acquire the resource, it simply spins in a loop on the CPU. This wastes valuable CPU time that could be used for other work or to conserve power.
- Kernel-Mode Constructs: These are provided directly by the Windows OS. To use them, your application's threads must transition from managed code to native user-mode code, and then into kernel-mode code. This transition incurs a massive performance penalty. However, they have a massive upside: if a thread cannot acquire a resource, Windows puts the thread to sleep, preventing it from spinning and wasting CPU time.
Boolean, Int32, and reference types) are atomic—meaning all bytes are read or written at once. However, compiler and CPU optimizations can execute these atomic operations at unexpected times. User-mode constructs enforce strict timing on these operations.true before the accompanying data has actually been written to memory.System.Threading.Volatile class, offering Read and Write methods. The Volatile Rule: When threads communicate via shared memory, write the last value by calling Volatile.Write and read the first value by calling Volatile.Read. This disables the dangerous compiler and CPU optimizations that reorder instructions or cache values in registers.volatile keyword as syntactical sugar for this, many expert developers (including Richter) heavily discourage its use. Marking a field as volatile turns every read and write into a volatile operation, which hurts performance. Furthermore, volatile fields cannot be passed by reference (using out or ref), and they are not Common Language Specification (CLS) compliant.Volatile methods perform either an atomic read or an atomic write, the System.Threading.Interlocked class performs an atomic read and write simultaneously. Every method in the Interlocked class acts as a full memory fence, meaning no variable reads or writes can be reordered across the method call.Interlocked class provides incredibly fast, thread-safe methods like Increment, Decrement, Add, Exchange, and CompareExchange (which conditionally replaces a value only if it matches a specified comparand). Because these methods avoid locks entirely, you can use them to build highly scalable asynchronous architectures that can handle thousands of concurrent requests without ever blocking a thread.Interlocked methods, you can build your own thread synchronization lock. By continuously looping and calling Interlocked.Exchange on an Int32 field, a thread can attempt to flip a 0 (free) to a 1 (in-use). The first thread to see Exchange return 0 successfully acquires the lock, while all other threads spin continuously in the while (true) loop.System.Threading.SpinWait structure implements this by calling a mixture of Thread.Sleep, Thread.Yield, and Thread.SpinWait. The FCL offers a robust System.Threading.SpinLock value type that uses this exact black magic to optimize performance while offering timeout support.Interlocked doesn't provide a method for it? You can use the Interlocked Anything Pattern.Interlocked.CompareExchange inside a do...while loop.- You read the current value into a local variable.
- You perform your complex operation on that local variable and store the desired result.
- You call
CompareExchangeto swap the new value into the shared field, only if the shared field hasn't been changed by another thread since you started step 1. - If another thread did change the value,
CompareExchangefails, the loop restarts, and you perform the calculation again with the freshest data.
- They put waiting threads to sleep, preventing wasted CPU time.
- They can synchronize threads across different processes on the same machine.
- They allow threads to block with a specified timeout.
- They support security permissions.
System.Threading.WaitHandle class. This class wraps a Win32 kernel object handle and exposes powerful methods like WaitOne, WaitAll, and WaitAny.Semaphore, EventWaitHandle, or Mutex and assigning it a unique string name, multiple processes can attempt to open the same kernel object. Windows guarantees only one thread creates it; the second process will see that the object already exists and can immediately exit, knowing another instance is already running.- AutoResetEvent: When this event becomes
true, it wakes up exactly one blocked thread and then the kernel immediately automatically resets the event back tofalse. - ManualResetEvent: When this event becomes
true, it wakes up all blocked threads. It remainstrueuntil your code manually resets it back tofalse.
AutoResetEvent unblocks one thread, releasing a Semaphore unblocks a specific number of threads (determined by the releaseCount passed to the Release method).Mutex represents a mutually exclusive lock. It operates similarly to an AutoResetEvent by releasing only one waiting thread at a time, but it comes with substantial additional baggage.Mutex explicitly records which thread currently owns it. If a thread attempts to release a Mutex it doesn't own, an exception is thrown. Furthermore, Mutex objects support recursion. If the owning thread waits on the Mutex again, it increments an internal recursion count and continues running. The thread must release the Mutex the exact same number of times before another thread can claim it.Mutex notoriously slow. If you need a recursive lock, it is significantly faster to implement the recursion tracking in managed code (using a construct like RecursiveAutoResetEvent) so that the thread only transitions into the Windows kernel when it actually needs to block.SimpleHybridLock class that contains two fields: an Int32 (manipulated via primitive user-mode constructs) and an AutoResetEvent (a primitive kernel-mode construct).Interlocked.Increment on the Int32 field. If the thread sees that there were zero threads waiting, it acquires the lock immediately and returns. The thread acquires the lock incredibly quickly without ever transitioning into the Windows kernel.WaitOne on the AutoResetEvent. This forces the thread to transition into the Windows kernel and block. While this transition is a significant performance hit, the thread had to stop running anyway to wait for the resource, so putting it to sleep prevents it from spinning and wasting valuable CPU time.SimpleHybridLock immediately creates the AutoResetEvent, which is a massive performance hit. In professional implementations, the creation of the kernel-mode construct is deferred until the very first time contention is actually detected.Mutex is an example of a lock with these features. However, adding ownership and recursion requires tracking additional state, which increases memory consumption and heavily degrades the lock's performance.ManualResetEventSlim and SemaphoreSlim ClassesCancellationToken integration, allowing a waiting thread to be forcibly unblocked.Monitor Class and Sync BlocksMonitor class is arguably the most-used hybrid construct in .NET. It provides a mutually exclusive lock that supports spinning, thread ownership, and recursion.Monitor.Enter is called on an object, the CLR associates a free "sync block" (a data structure containing the kernel object, owning thread ID, and recursion count) from a process-wide array to that object. When Monitor.Exit is called and no other threads are waiting, the sync block is detached and returned to the pool.Monitor is popular, it is fraught with dangerous architectural flaws because it is a static class that can lock on any object:- Public Locks: If you lock on
this, your lock is publicly exposed. Malicious or poorly written external code can also lock on your object, deadlocking your application. Always use a private object for locking. - String Interning: Because identical strings can be interned to the same memory reference, two completely independent pieces of code locking on the string
"MyLock"are actually synchronizing with each other unknowingly. - AppDomain Leaks:
Monitorviolates AppDomain isolation. Locking onTypeobjects orStringreferences can inadvertently synchronize threads across different AppDomains.
lock keyword, which is syntactical sugar that wraps Monitor.Enter and Monitor.Exit in a try/finally block. Microsoft did this to ensure locks are always released. However, if an exception is thrown inside the try block while your thread is halfway through mutating shared state, the state is now corrupted. The finally block gracefully releases the lock, immediately allowing other threads to access the corrupted state, resulting in unpredictable behavior and security holes. It is often safer for an application to hang (deadlock) than to continue executing with corrupted state.ReaderWriterLockSlim ClassReaderWriterLockSlim solves this by enforcing the following rules:- When a thread is writing, all other readers and writers are blocked.
- When a thread is reading, other readers are allowed in concurrently, but writers are blocked.
- When all reading threads finish, a waiting writer is unblocked.
ReaderWriterLockSlim, you should always pass LockRecursionPolicy.NoRecursion. Supporting recursion on a reader-writer lock is phenomenally expensive because the lock must track every single reader thread and its individual recursion count.OneManyLock ClassReaderWriterLockSlim has overhead, Jeffrey Richter created his own implementation called OneManyLock. It packs the entire state of the lock (owned by writer, number of readers, waiting readers, waiting writers) into a single Int64 field. By manipulating this single field atomically using Interlocked.CompareExchange, the lock is incredibly fast and only falls back to a Semaphore (for readers) or an AutoResetEvent (for writers) when absolute blocking is necessary.CountdownEvent and Barrier- CountdownEvent: Internally using a
ManualResetEventSlim, this construct blocks a thread until its internal counter reaches zero. It acts as the exact opposite of a Semaphore. - Barrier: Used for phased parallel algorithms. If multiple threads are working on a staged task (like the CLR's garbage collector), a
Barrierforces threads that finish Phase 1 quickly to block until all other threads have also finished Phase 1 before any thread is allowed to proceed to Phase 2.
- Do not label your threads. Do not create a "spell-check thread" or a "database thread." Use the thread pool to rent threads for brief periods.
- If you must mutate state, try to use fast, non-blocking
VolatileandInterlockedmethods. - If you must block, use
Monitorwith a completely private lock object. - Avoid recursive locks, and avoid releasing locks in
finallyblocks if state corruption has occurred.
Volatile.Write when publishing the reference to the newly created singleton object. Without Volatile.Write, compiler and CPU optimizations might publish the memory address of the singleton object before the object's constructor has finished executing. Another thread could grab this reference and attempt to use a partially constructed object, causing horrific, hard-to-track timing bugs.Interlocked.CompareExchange. Multiple threads might briefly race and create duplicate singleton objects on the heap, but CompareExchange ensures only one definitively wins and gets published; the losers' objects are simply garbage collected. Because no threads ever block on a kernel construct, performance remains stellar.Interlocked.Monitor.Wait, Monitor.Pulse, and Monitor.PulseAll.- A thread acquires a mutually exclusive lock (
Monitor.Enter). - It tests the complex condition. If false, it calls
Monitor.Wait. This brilliantly releases the lock so other threads can mutate the state, while simultaneously putting the waiting thread to sleep. - Another thread enters the lock, modifies the state to satisfy the condition, and calls
Monitor.PulseAllto wake up waiting threads before exiting the lock. - The original thread wakes up, re-acquires the lock automatically, and loops back to test the condition again.
SemaphoreSlim provides a WaitAsync() method. Instead of blocking the current thread, WaitAsync() returns a Task. You use C#'s await keyword on it. If the lock is free, your code continues executing normally. If the lock is held, the thread is released back to the thread pool to do other work. When the lock becomes free, a thread pool thread automatically resumes your state machine.ConcurrentExclusiveSchedulerPair. You pass its ExclusiveScheduler or ConcurrentScheduler to a TaskFactory depending on whether you need write or read access. Alternatively, custom implementations like Richter's AsyncOneManyLock allow you to elegantly await asyncLock.AcquireAsync(OneManyMode.Shared).System.Collections.Concurrent namespace: ConcurrentQueue<T>, ConcurrentStack<T>, ConcurrentDictionary<TKey, TValue>, and ConcurrentBag<T>.TryDequeue and TryGetValue return immediately—giving you the item and returning true if successful, or returning false if the collection is empty. No thread is ever forced to sit idle waiting for an item to appear.BlockingCollection<T>. The BlockingCollection<T> uses SemaphoreSlim objects internally to block the threads. When the producer finishes adding data, it calls CompleteAdding(), which safely signals all sleeping consumers to wake up, finish consuming the remaining items, and terminate