C# / .NET Interview Questions and Answers: Part 1 – Core Language & Platform Fundamentals

Welcome to the first article in our series exploring the ins and outs of C# and .NET! This article will dive into essential concepts every .NET developer should know. Whether you're gearing up for interviews or just looking to improve your knowledge, we've got you covered. This article will focus on C# Core Language and .NET Platform Fundamentals questions.
What is C#? How does it differ from languages like C or C++?
C# is a modern, managed, type-safe programming language developed by Microsoft for the .NET platform, designed to prioritize developer productivity, safety, and robust application development. Unlike C and C++, unmanaged languages offer low-level control with manual memory management. C# leverages the Common Language Runtime (CLR) to provide automatic memory management through garbage collection, type safety to prevent common errors, and a simplified syntax inspired by C-style languages. While C# retains familiar C/C++ constructs like classes and pointers (in unsafe contexts), it abstracts away complexities such as direct memory manipulation, making it better suited for rapid development of secure, scalable applications across platforms, whereas C/C++ excels in performance-critical, system-level programming.
What .NET engineers should know:
- ๐ผ Junior: Should understand that C# is a high-level, managed language used primarily for .NET applications, with automatic memory management, contrasting with C/C++โs manual memory handling. Recognize fundamental syntactic similarities (e.g., loops, conditionals), but note C#โs focus on ease of use.
- ๐ Middle: Expected to grasp key differences, including C#โs CLR-based execution, garbage collection, and type safety versus C/C++โs direct hardware access and potential for memory leaks. Should understand use cases: C# for enterprise/web apps, C/C++ for system-level or performance-critical code.
- ๐ Senior: Should deeply understand C#โs design goals (productivity, safety) and CLRโs role in enabling cross-platform development, contrasting with C/C++โs trade-offs (performance vs. complexity). Able to evaluate when to use C# vs. C/C++ (e.g., C# for rapid development, C++ for low-latency systems) and leverage C#โs unsafe code or interop for performance-critical scenarios.
๐Resources: What makes C# Different from Other Languages?
NET Framework vs .NET Core vs .NET 5+: What You Need to Know?

The .NET Framework is a software development platform developed by Microsoft for building and running Windows applications. .NET Core is a cross-platform, open-source successor to the .NET Framework, designed for building Windows, Linux, and macOS applications. Starting with .NET 5, Microsoft unified the platform, combining the best of the .NET Framework and .NET Core into a single, cross-platform framework. Subsequent versions (.NET 6, 7, 8, 9) introduced further improvements and features.โ
What .NET engineers should know about difference between .NET versions:
- ๐ผ Junior: Should know that .NET is a platform for building applications, with .NET Framework being Windows-only and .NET Core/.NET 5+ being cross-platform.โ
- ๐ Middle: Expected to understand the differences between .NET Framework and .NET Core, the reasons for the unification in .NET 5, and the benefits of migrating to newer versions.โ
- ๐ Senior: Should have insights into the architectural changes introduced in .NET 5 and beyond, the impact on existing applications, and strategies for migration and optimization.
๐Resources: Differences between .NET vs .NET Core vs .NET Standard vs .NET Framework and resolving the confusion
What is the Common Language Runtime (CLR)?
The Common Language Runtime (CLR) is the runtime environment for .NET applications, and it is responsible for managing the execution of programs written in languages like C#. It provides essential services such as memory management, security enforcement, exception handling, and garbage collection. In the .NET Framework, the CLR was Windows-centric and optimized for enterprise applications. With .NET Core and .NET 5+, the CoreCLR replaced it, introducing a cross-platform, open-source runtime with significant performance improvements, including tiered compilation (balancing startup time and runtime optimization) and enhanced scalability for cloud and containerized environments. The CoreCLR continues to evolve in .NET 9, supporting modern features like Native AOT for faster startup and reduced memory usage.


What .NET engineers should know about CLR:
- ๐ผ Junior: Should know that the CLR (and CoreCLR in modern .NET) is the engine that runs .NET applications, handling tasks like memory management, security, and program execution, making development easier than in unmanaged environments.
- ๐ Middle: Expected to understand how the CLR/CoreCLR works, including concepts like Just-In-Time (JIT) compilation (with tiered compilation in CoreCLR), managed code, and the role of assemblies. CoreCLR should recognize its cross-platform capabilities and performance advantages over the .NET Framework CLR.
- ๐ Senior: Should have an in-depth understanding of the CLR/CoreCLR architecture, including differences between the .NET Framework CLR (Windows-only) and CoreCLR (cross-platform, optimized). You should know how to optimize applications by leveraging CoreCLR features like tiered compilation, profile-guided optimization, and interaction with the operating system for high-performance scenarios.
๐Resources:
- Common Language Runtime (CLR) overview
- https://stackoverflow.com/questions/48908739/clr-vs-core-clr
- https://github.com/dotnet/coreclr
What is the Common Type System (CTS)?
The Common Type System (CTS) defines how types are declared, used, and managed in the .NET runtime, ensuring interoperability between languages by providing a common framework for type definitions and operations.โ

What .NET engineers should know about CTS:
- ๐ผ Junior: Should know that CTS provides a standard for type definitions across .NET languages, enabling interoperability.โ
- ๐ Middle: Expected to understand how CTS ensures that types in different .NET languages can interact seamlessly and how it enforces type safety and consistency.โ
- ๐ Senior: Should be able to design systems that leverage CTS for cross-language interoperability, understanding the implications for type compatibility, performance, and versioning.โ
๐Resources:
Common Type System - Microsoft Learn
What is the Common Language Specification (CLS)?
The Common Language Specification (CLS) is a set of rules and constraints that languages must adhere to to be compatible with the .NET framework. It ensures that code written in one language can be interoperated with any language supported by .NET.โ

What .NET engineers should know about CLS:
- ๐ผ Junior: Should understand that CLS ensures interoperability between .NET languages by defining standard features and rules.โ
- ๐ Middle: Expected to write CLS-compliant code to maximize compatibility across different .NET languages and understand the restrictions imposed by CLS.โ
- ๐ Senior: Should be able to design CLS-compliant libraries and frameworks, ensuring broad usability across various .NET languages and understanding the trade-offs involved.โ
๐Resources: Common Language Specification
What is a Just-In-Time (JIT) compilation in .NET?
Just-In-Time (JIT) compilation is the process by which the Common Language Runtime (CLR) or CoreCLR converts Intermediate Language (IL) code into native machine code specific to the operating system and CPU architecture at runtime, just before execution. This enables .NET applications to remain platform-independent at the IL level while executing efficiently on the target platform. In modern .NET (e.g., .NET 9), the RyuJIT compiler powers JIT compilation, offering optimizations for performance. Tiered Compilation, introduced in .NET Core 2.1, dynamically balances startup time and runtime performance by initially compiling methods with minimal (fast startup) optimizations and later recompiling hot paths with advanced optimizations for better throughput.
Pre-JIT Compiler:

Normal JIT Compiler:


What .NET engineers should know about JIT compilers:
- ๐ผ Junior: Should understand that JIT compilation translates IL code to machine code at runtime, allowing .NET applications to run on any supported platform with the help of the CLR/CoreCLR.
- ๐ Middle: Expected to know the types of JIT compilation (e.g., Normal JIT with RyuJIT, Pre-JIT via NGEN/ReadyToRun) and their impact on startup time and performance. Should understand Tiered Compilationโs role in optimizing code dynamically for better efficiency.
- ๐ Senior: Should be able to optimize application performance by leveraging JIT compilation intricacies, such as method inlining, loop unrolling, and Tiered Compilationโs adaptive optimizations. Should also utilize tools like Native Image Generator (NGEN) or ReadyToRun images for precompilation and understand RyuJITโs advanced optimizations in .NET 5+ for high-performance scenarios.
Explain the Native AOT (Ahead-of-Time)
Native AOT (Ahead-of-Time) Compilation in .NET 8+ allows applications to be compiled directly to native machine code before execution, bypassing the Just-In-Time (JIT) compilation traditionally performed at runtime. This approach offers several benefits:
- Improved Startup Time: By eliminating the need for JIT compilation, applications start faster, making them ideal for scenarios requiring rapid initialization.
- Reduced Memory Usage: Native AOT produces self-contained executables with a smaller memory footprint, benefiting environments with limited resources, such as containers or edge devices.
- Single-File Deployment: Applications can be packaged into a single native executable, simplifying deployment and distribution.

Native AOT is particularly advantageous for scenarios where predictable performance and minimal resource consumption are critical, such as microservices, cloud-native applications, or serverless workloads. However, it has limitations: reflection support is restricted, requiring static code patterns, and dynamic scenarios (e.g., runtime type loading or serialization) may face compatibility issues, necessitating careful design to avoid runtime dependencies.
What .NET engineers should know AOT compilers:
- ๐ผ Junior: Should understand that Native AOT compiles .NET code to native executables before running, improving startup and reducing resource use compared to JIT compilation.
- ๐ Middle: Expected to recognize Native AOTโs benefits for performance-critical applications and its limitations, such as reduced reflection capabilities, and know when to choose AOT over JIT based on project needs.
- ๐ Senior: Should be able to design AOT-compatible applications, mitigating limitations like restricted reflection by using source generators or static APIs, and optimize deployment for microservices or edge scenarios while understanding trade-offs in dynamic behavior.
๐Resources:
- Native AOT deployment overview
- ๐ฝ๏ธDeep dive on native AOT
- Mastering .NET Native AOT: Benefits and Examples
What are assemblies in .NET? Explain the Global Assembly Cache (GAC).
An assembly in .NET is a compiled code library that is the fundamental unit for deployment, versioning, and security. It can be a Dynamic Link Library (DLL) or an executable (EXE) and contains metadata describing the types, resources, and dependencies it exposes. Assemblies enable modularity and portability in .NET applications. The Global Assembly Cache (GAC) is a machine-wide repository in .NET Framework for storing strongly named assemblies intended to be shared across multiple applications, supporting centralized management and versioning. However, in modern .NET (e.g., .NET 5+), the GAC is primarily deprecated, as NuGet packages and self-contained deployments provide more flexible and portable dependency management, reducing reliance on a centralized cache.
What .NET engineers should know about GAC:
- ๐ผ Junior: Should understand that assemblies are compiled code libraries (DLLs or EXEs) containing .NET code and metadata and that the GAC was historically used to store shared assemblies in .NET Framework, though less relevant today.
- ๐ Middle: Expected to know how to create strongly named assemblies and, for legacy .NET Framework scenarios, install them into the GAC using tools like
gacutil.exe
. Should also understand modern dependency management with NuGet for .NET 5+ projects. - ๐ Senior: Should be able to manage assembly versions and troubleshoot loading conflicts in .NET Frameworkโs GAC while prioritizing modern practices like NuGet and self-contained deployments for .NET 5+. Understand the implications of side-by-side execution in the GAC and guide teams toward portable, maintainable solutions.
๐Resources:
How to install an assembly into the global assembly cache
What is the Difference Between Managed and Unmanaged Code?
Managed code is executed by the Common Language Runtime (CLR) or CoreCLR, which provides services like garbage collection, type safety, and exception handling, simplifying development and enhancing security. Unmanaged code, such as applications written in languages like C or C++ and compiled to native machine code, runs outside the CLRโs management, directly on the CPU with operating system scheduling. This requires manual resource handling, such as memory allocation and deallocation, increasing complexity but offering greater control.

What .NET engineers should know about managed & unmanaged code:
- ๐ผ Junior: Should understand that managed code runs under the CLR with automatic memory management, while unmanaged code operates without CLR oversight, requiring developers to handle resources manually.
- ๐ Middle: Expected to understand the implications of unmanaged code, including manual memory management and potential interoperability with managed code through Platform Invocation Services (P/Invoke) or COM interop for integrating native libraries.
- ๐ Senior: Should be able to discuss scenarios where unmanaged code is necessary (e.g., performance-critical systems, hardware access), the risks involved (e.g., memory leaks, pointer errors), and strategies for securely and efficiently integrating unmanaged code into managed applications using techniques like
SafeHandle
or marshaling.
๐Resources:
Explain the difference between Value Types and Reference Types in .NET.
Value types store data directly and are typically allocated on the stack, including types like int
, double
, and structs
. Reference types store references to the actual data, which is allocated on the heap, including types like class
, interface
, and delegate
.โ
What .NET engineers should know about the difference between value & reference types in .NET:
- ๐ผ Junior: Understand the fundamental distinction between value and reference types.โ
- ๐ Middle: Expected to know how value and reference types affect memory allocation, parameter passing, and performance.โ
- ๐ Senior: Should be able to make informed decisions on when to use value or reference types based on factors like memory usage, performance considerations, and application architecture.
๐Resources:
Types in .NET - Microsoft Learn
What is Garbage Collection in .NET, and how does it work?
Garbage Collection (GC) in .NET is an automatic memory management feature that reclaims memory occupied by objects no longer in use, preventing memory leaks and simplifying development. It operates on a generational approach, categorizing objects based on lifespan to optimize performance. Modern .NET supports different GC modesโWorkstation GC (optimized for client applications with low latency) and Server GC (designed for high-throughput server applications with multiple cores)โas well as Background GC, which reduces pauses by performing collections concurrently with application execution, enhancing responsiveness in high-performance scenarios.
The .NET Garbage Collector divides memory into three generations:
- Generation 0: Short-lived objects, such as temporary variables or method call allocations, are collected frequently for efficiency.
- Generation 1: Objects that have survived one or more garbage collection cycles, typically mid-lived, acting as a buffer between short- and long-lived objects.
- Generation 2: Long-lived objects that have survived several GC cycles, such as static data or objects persisting throughout the applicationโs life, are collected less often to reduce overhead.
The Garbage Collection Process
The GC process consists of the following steps:
- Mark: The GC identifies all objects still referenced in the application, marking them as in use.
- Sweep: The GC scans the heap to find unmarked objects (no longer referenced) and marks them for removal.
- Compact: To reduce memory fragmentation, the GC moves live objects closer together, freeing up larger contiguous memory blocks for future allocations.
In Background GC, these steps (especially for Generation 2) occur concurrently with application threads, minimizing pauses in server or interactive applications. Server GC parallelizes collections across multiple cores, which is ideal for high-throughput systems like web servers, while Workstation GC prioritizes responsiveness for desktop apps.
See the sequence diagram below:

What .NET engineers should know about Garbage Collection in .NET:
- ๐ผ Junior: Should know that GC automatically frees memory by removing unused objects, making .NET development easier and safer compared to manual memory management.
- ๐ Middle: Expected to understand the generational model (Generation 0, 1, and 2), how it impacts performance, and the differences between Workstation GC (low-latency) and Server GC (high-throughput). Should recognize Background GCโs role in reducing pauses.
- ๐ Senior: Should have deep knowledge of GC mechanisms, including optimizing memory usage with Workstation vs. Server GC, handling Large Object Heap (LOH) efficiently, and using diagnostics (e.g.,
GCSettings
,dotnet-counters
) for performance tuning in high-throughput or latency-sensitive applications.
๐Resources:
- Fundamentals of garbage collection
- Understanding Garbage Collection in .NET: How to Optimize Memory Management
- ๐ฝ๏ธ Garbage Collection in C#
What is the difference between an Abstract Class and an Interface in C#?
An abstract class can provide both abstract (unimplemented) and concrete (implemented) methods, enabling shared code among related classes while preventing instantiation. An interface defines a contract with methods, properties, events, or indexers that implementing classes must fulfill. Before C# 8.0, interfaces could not include implementations, but with default interface methods (introduced in C# 8.0), interfaces can now provide optional implementations, blurring the line with abstract classes. This allows interfaces to evolve without breaking existing implementations, but unlike abstract classes, interfaces still cannot hold state (fields) and support multiple inheritance, influencing design choices: use abstract classes for shared state and behavior in a hierarchy, and interfaces for flexible, cross-type contracts, leveraging default methods for backward-compatible extensions.
What .NET engineers should know:
- ๐ผ Junior: Should know that abstract classes can have some implemented methods and are single-inheritance, while interfaces (before C# 8.0) only declare methods and support multiple inheritance. Understand that default interface methods add optional implementations.
- ๐ Middle: Expected to understand that a class can implement multiple interfaces but inherit from only one abstract class and recognize when to use default interface methods to extend functionality without altering existing code versus abstract classes for shared logic and state.
- ๐ Senior: Should be able to design systems using abstract classes for hierarchical code reuse and interfaces for decoupled, extensible contracts, leveraging default interface methods to evolve APIs safely. When choosing between them, consider factors like state management, multiple inheritance, and adherence to SOLID principles.
Explain the concept of Boxing and Unboxing in C#

Boxing is converting a value type (e.g., int
, double
) to a reference type (object), while unboxing is the reverse processโconverting a reference type back to a value type. Boxing wraps the value inside an object, and unboxing extracts the value from the object.
int num = 123;
object boxed = num; // Boxing
int unboxed = (int)boxed; // Unboxing
What .NET engineers should know about boxing and unboxing:
- ๐ผ Junior: You should understand that boxing and unboxing are mechanisms to convert between value types and reference types.โ
- ๐ Middle: Expected to know that boxing involves allocating memory on the heap, which can affect performance and should be minimized in performance-critical applications.โ
- ๐ Senior: Should be able to identify scenarios where boxing and unboxing occur implicitly and design code to avoid unnecessary boxing/unboxing to optimize performance.
๐Resources:
Boxing and Unboxing - Microsoft Learn
What is the difference between const
and readonly
in C#?
The const
keyword defines compile-time constants, meaning their values are set at compile time and cannot change thereafter. readonly
fields are runtime constants; their values can be assigned during declaration or within a constructor, allowing for different values depending on the constructor used.
public class Example
{
public const int ConstValue = 10;
public readonly int ReadonlyValue;
public Example(int value)
{
ReadonlyValue = value;
}
}
What .NET engineers should know:
- ๐ผ Junior: should know that
const
is for constants known at compile time, whilereadonly
allows assignment at runtime, typically in constructors.โ - ๐ Middle: Expected to understand that
const
fields are static by default and cannot be changed, whereasreadonly
fields can have different values for each instance of a class.โ - ๐ Senior: Should be able to decide appropriately between using
const
andreadonly
based on the immutability requirements and the timing of value determination (compile-time vs. runtime).
What is the purpose of the using
statement in C#?
The using
statement serves two purposes: it allows for the inclusion of namespaces, enabling access to classes and methods within those namespaces, and it provides a means to ensure the correct use of IDisposable objects by automatically disposing of them when they are no longer needed, thus managing unmanaged resources efficiently.
using
usage example:
using System;
using (var file = new StreamReader("file.txt"))
{
// Use the file
} // file is disposed here
What .NET engineers should know about using
statements:
- ๐ผ Junior: Should know that the
using
statement helps include namespaces and manage the disposal of resources like files or database connections.โ - ๐ Middle: Expected to understand how the
using
statement ensures deterministic disposal of resources and its importance in preventing memory leaks.โ - ๐ Senior: Should be able to implement the IDisposable interface in custom classes and utilize the
using
statement to manage both managed and unmanaged resources effectively.
What is Exception Handling in C#, and how is it implemented?
Exception handling in C# is a mechanism to handle runtime errors, ensuring the normal flow of the application is maintained. It is implemented using try
, catch
, finally
, and throw
keywords. Code that might throw an exception is placed inside a try
block, and exceptions are caught using catch
blocks, and the finally
block is used for cleanup code that executes regardless of whether an exception was thrown.โ
try
{
// Code that may throw an exception
}
catch (Exception ex)
{
// Handle the exception
}
finally
{
// Cleanup code
}
What .NET engineers should know about exception handling in C#:
- ๐ผ Junior: Should know how to use
try
andcatch
blocks to handle exceptions and ensure the application doesn't crash unexpectedly.โ - ๐ Middle: Expected to implement specific exception handling, create custom exceptions, and use the
finally
block for resource cleanup.โ - ๐ Senior: Should be able to design a global exception-handling strategy, understand the performance implications of exceptions, and implement best practices for exception handling across large applications.
๐Resources:
What is the difference between throw
and throw ex
in C#?
Using throw
rethrows the current exception while preserving the original stack trace, which is crucial for debugging. Using throw ex
resets the stack trace, making it appear as though the exception from the throw ex
statement, which can obscure the original error location.

What .NET engineers should know about difference between throw
and throw ex
in C#:
- ๐ผ Junior: Should understand that
throw
preserves the original error information whilethrow ex
can obscure it.โ - ๐ Middle: Expected to be used
throw
to maintain accurate stack traces and avoidthrow ex
unless there's a specific reason to reset the stack trace.โ - ๐ Senior: Should be able to design exception-handling strategies that ensure accurate error reporting and debugging information, understanding the implications of rethrowing exceptions.
What is the difference between finalize
and dispose
methods in .NET?
Finalize
is a method the garbage collector uses to clean up unmanaged resources before an object is reclaimed. It is non-deterministic, meaning its execution timing is unpredictable, as it depends on the garbage collectorโs schedule. Dispose
, part of the IDisposable
interface, enables the deterministic release of managed and unmanaged resources, typically called explicitly by the consumer or via using
a statement. The Dispose Pattern ensures proper resource cleanup by separating managed and unmanaged resources and preventing redundant operations, with Dispose
suppressing finalization when resources are already freed.
public class ResourceWrapper : IDisposable
{
private bool _disposed;
private IntPtr _unmanagedHandle; // Example unmanaged resource
private SomeManagedResource _managedResource; // Example managed resource
public ResourceWrapper()
{
_unmanagedHandle = AllocateUnmanagedResource(); // Simulate unmanaged allocation
_managedResource = new SomeManagedResource(); // Simulate managed resource
}
// Implement IDisposable
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // Prevent finalizer from running if Dispose is called
}
// Dispose pattern implementation
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
// Clean up managed resources
_managedResource?.Dispose(); // Dispose managed resources only when disposing = true
}
// Clean up unmanaged resources
if (_unmanagedHandle != IntPtr.Zero)
{
FreeUnmanagedResource(_unmanagedHandle);
_unmanagedHandle = IntPtr.Zero;
}
_disposed = true;
}
// Finalizer (destructor)
~ResourceWrapper()
{
Dispose(false); // Only clean up unmanaged resources in finalizer
}
// Simulated resource allocation and cleanup methods
private IntPtr AllocateUnmanagedResource() => IntPtr.Zero; // Placeholder
private void FreeUnmanagedResource(IntPtr handle) { /* Cleanup logic */ }
}
public class SomeManagedResource : IDisposable
{
public void Dispose() { /* Cleanup logic */ }
}
What .NET engineers should know:
- ๐ผ Junior: Should know that
Dispose
is used for manual resource cleanup (called explicitly), whileFinalize
is used by the garbage collector for unmanaged resources ifDispose
isnโt called. - ๐ Middle: Expected to implement the
IDisposable
interface correctly, understand the role of the finalizer for unmanaged resources and apply the Dispose Pattern to manage both managed and unmanaged resources. - ๐ Senior: Should design classes that efficiently handle managed and unmanaged resources using the Dispose Pattern, optimize resource cleanup to avoid leaks, and understand the interplay between
Dispose
andFinalize
, including when to useSafeHandle
for safer unmanaged resource management.
๐Resources:
- Implementing a Dispose method - Microsoft Docs
- Finalizers are tricker than you might think. Part 1
- Finalizers are tricker than you might think. Part 2
What are extension methods in C#, and how are they used?
Extension methods in C# allow you to "extend" an existing class with new methods without altering its source code or creating a derived class. This feature is useful for adding functionality to third-party libraries or built-in types.
Code Example:
public static class StringExtensions
{
public static bool IsNumeric(this string input)
{
return int.TryParse(input, out _);
}
}
// Usage
string value = "12345";
bool result = value.IsNumeric(); // returns true
What .NET engineers should know about extension methods in C#:
- ๐ผ Junior: Know basic syntax for creating and using extension methods.
- ๐ Middle: Know how extension methods are resolved at compile time and understand their limitations, such as being unable to override existing methods. Recognize appropriate scenarios for extension methods (e.g., enhancing readability and maintainability).
- ๐ Senior: Be able to evaluate scenarios where extension methods might impact maintainability negatively if misused.
๐ Resources:
Describe how Source Generators work in C#
Source Generators, introduced in C# 9 and .NET 5, are compile-time components that inspect user code and generate additional source code during compilation. They enable metaprogramming by analyzing existing code to produce new code, enhancing performance by reducing or eliminating runtime reflection and minimizing boilerplate. Source Generators are particularly effective for static scenarios, offering a faster, type-safe alternative to reflectionโs runtime overhead.
Example:
[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context) { }
public void Execute(GeneratorExecutionContext context)
{
var source = @"
namespace GeneratedCode
{
public static class HelloWorld
{
public static void SayHello() => Console.WriteLine(""Hello, World!"");
}
}";
context.AddSource("HelloWorldGenerator", source);
}
}
In this example, the HelloWorldGenerator
generates a static class with a method that prints "Hello, World!" to the console. This generated code becomes part of the compilation process, allowing developers to invoke GeneratedCode.HelloWorld.SayHello()
in their applications.
What .NET engineers should know:
- ๐ผ Junior: Understand that Source Generators create code automatically during compilation, reducing manual work and improving performance compared to runtime techniques like reflection.
- ๐ Middle: Expected to implement basic Source Generators to automate repetitive coding tasks, replacing reflection where possible to improve efficiency and reduce runtime overhead.
- ๐ Senior: Should design and integrate complex Source Generators into large-scale projects, using them to enforce code consistency, optimize performance by eliminating reflection, and leverage Roslyn APIs for advanced code analysis and generation.
๐Resources:
- Introducing C# Source Generators
- The .NET Compiler Platform SDK (Roslyn APIs)
- Source Generators in C# - Code Maze
What is reflection in C#, and what are its typical use cases?
Reflection in C# allows inspection of assemblies, types, methods, properties, and fields at runtime, enabling dynamic instantiation of types, method invocation, or attribute access without compile-time knowledge. While powerful, reflection incurs significant performance overhead due to runtime type resolution, making it slower than static code. For performance-critical scenarios, alternatives like Source Generators (for compile-time code generation) or System.Linq.Expressions
(for dynamic method invocation with better performance) should be considered.
using System;
using System.Reflection;
class Example
{
public void Hello(string name) => Console.WriteLine($"Hello, {name}!");
}
// Usage
Type type = typeof(Example);
object instance = Activator.CreateInstance(type);
MethodInfo method = type.GetMethod("Hello");
method.Invoke(instance, new object[] { "World" });
// Output: Hello, World!
Typical Use Cases:
- Building dynamic type loaders and plug-in systems.
- Implementing serialization and deserialization mechanisms.
- Inspecting metadata of assemblies for dynamic code analysis or testing frameworks
What .NET engineers should know about reflection:
- ๐ผ Junior: Should understand reflection as a technique for inspecting and manipulating types at runtime, with basic familiarity with creating objects dynamically (e.g., using
Activator.CreateInstance
). - ๐ Middle: Expected to use reflection for tasks like dynamically invoking methods, accessing properties, or inspecting attributes while being aware of its performance costs and considering alternatives like Source Generators for repetitive tasks.
- ๐ Senior: Should evaluate when reflection is necessary despite its overhead and optimize its use (e.g., caching
MethodInfo
), and prefer alternatives like Source Generators for compile-time code generation or Expression Trees for dynamic invocation in performance-critical systems.
๐ Resources:
Explain the concept of delegates and events in C#
Delegates in C# represent method references, enabling methods to be passed as parameters and invoked indirectly. Events are built on delegates and allow classes to notify subscribers about occurrences or state changes.
Delegate example:
public delegate void Notify(string message);
class Program
{
static void Main()
{
Notify notifier = DisplayMessage;
notifier("Hello, delegate!");
}
static void DisplayMessage(string message)
{
Console.WriteLine(message);
}
}
// Output: Hello, delegate!
Event example:
public class Publisher
{
public event EventHandler<string> Notify;
public void RaiseNotification(string message)
{
Notify?.Invoke(this, message);
}
}
class Subscriber
{
public void Subscribe(Publisher pub)
{
pub.Notify += OnNotificationReceived;
}
void OnNotificationReceived(object sender, string message)
{
Console.WriteLine($"Notification: {message}");
}
}
// Usage
var pub = new Publisher();
var sub = new Subscriber();
sub.Subscribe(pub);
pub.RaiseNotification("Event triggered!");
// Output: Notification: Event triggered!
What .NET engineers should know:
- ๐ผ Junior: Understand the basic usage of delegates and events for simple callback mechanisms.
- ๐ Middle: Can design and implement custom delegates and event handlers to facilitate communication between components. Understand multicast delegates and event invocation patterns.
- ๐ Senior: Able to design sophisticated event-driven systems, managing subscriber lifetimes to prevent memory leaks.
๐ Resources:
What is the difference between early binding and late binding in C#?
In C#, early binding (or compile-time binding) occurs when the compiler resolves method calls and property accesses during compilation. This means the compiler has complete knowledge of the object's type, ensuring type safety and allowing for compile-time optimizations, which can lead to better performance. On the other hand, late binding (or runtime binding) happens when the compiler cannot determine the object's type at compile time. Instead, the method calls or property accesses are resolved at runtime, typically using mechanisms like reflection or the dynamic
keyword. While late binding offers flexibility, it comes with a performance overhead and lacks compile-time type checking, potentially leading to runtime errors.
Early Binding Example:
// Early binding
var obj = new SomeClass();
obj.SomeMethod();
In this example, the compiler knows at compile time that obj
is of type SomeClass
and can directly map the SomeMethod
call to the corresponding method implementation. This allows for compile-time type checking and optimizations.
// Late binding using dynamic
dynamic obj = Activator.CreateInstance(Type.GetType("SomeNamespace.SomeClass"));
obj.SomeMethod();
Here, the type of obj
is determined at runtime. The SomeMethod
call is resolved during execution, which introduces flexibility and potential runtime errors if the method doesn't exist or if there's a type mismatch.
What .NET engineers should know about early binding and late binding in C#:
- ๐ผ Junior: You should understand that early binding refers to the compile-time resolution of method calls and property accesses, providing type safety and better performance, while late binding refers to runtime resolution, offering flexibility but with potential performance costs and the risk of runtime errors.โ
- ๐ Middle: Expected to recognize scenarios where late binding is appropriate, such as when working with COM objects or dynamically loaded assemblies, and understand the trade-offs involved, including the lack of compile-time type checking and potential performance implications.โ
- ๐ Senior: Should be able to implement late binding using reflection or the
dynamic
keyword, handle potential exceptions that may arise from runtime type issues, and design applications that balance the need for flexibility with the benefits of type safety and performance.
What is the difference between ref
and out
parameters in C#?
Both ref
and out
keywords are used to pass arguments by reference in C#, allowing methods to modify the value of the parameters. The key difference is that ref
requires that the variable be initialized before it is passed, whereas out
does not require prior initialization but mandates that the variable be assigned a value within the called method.
// Using ref
void ModifyRef(ref int number)
{
number += 10;
}
int valueRef = 5;
ModifyRef(ref valueRef);
// valueRef is now 15
// Using out
void InitializeOut(out int number)
{
number = 10;
}
int valueOut;
InitializeOut(out valueOut);
// valueOut is now 10
What .NET engineers should know:
- ๐ผ Junior: Should understand that
ref
requires initialization before passing whileout
does not, but the called method must assign a value to theout
parameter.โ - ๐ Middle: Expected to know scenarios where
ref
andout
are appropriately used, such as when a method must return multiple values or modify the caller's variable.โ - ๐ Senior: Should be able to design methods that effectively utilize
ref
andout
parameters, understanding the implications for method signatures, overloading, and maintainability.โ
๐Resources:
- ref and out parameters - Microsoft Learn
- Difference between ref and out parameters in C# - Stack Overflow
What is the purpose of the params
keyword in C#?
The params
keyword allows a method to accept a variable number of arguments as a single parameter, simplifying the method call when the exact number of arguments is unknown or variable. The parameters are passed as an array to the method.
void PrintNumbers(params int[] numbers)
{
foreach (int number in numbers)
{
Console.WriteLine(number);
}
}
// Usage
PrintNumbers(1, 2, 3, 4, 5);
What .NET engineers should know:
- ๐ผ Junior: The
params
keyword enables methods to accept various arguments.โ - ๐ Middle: Expected to understand how to implement methods using
params
and the implications for method overloading and parameter arrays.โ - ๐ Senior: Should be able to design flexible APIs that utilize the
params
keyword appropriately, considering performance implications and ensuring clarity in method usage.โ
๐Resources:
What is the purpose of the static
keyword in C#?
The static
keyword in C# denotes that a member (method, property, field) or a class belongs to the type rather than any specific instance. This means static members are shared across all instances and can be accessed without creating an object of the class.
public class Calculator
{
public static int Add(int a, int b)
{
return a + b;
}
}
// Usage
int sum = Calculator.Add(5, 10);
What .NET engineers should know:
- ๐ผ Junior: Should know that static members belong to the class and can be accessed without instantiating the class.
- ๐ Middle: Expected to understand scenarios where static members are appropriate, such as utility functions or shared data, and the implications for memory management.โ
- ๐ Senior: Should be able to design classes with appropriate use of static members, considering thread safety, initialization order, and potential pitfalls like static constructors and memory leaks.
๐Resources:
static (C# Reference) - Microsoft Learn
Explain the concepts of Covariance and Contravariance in .NET.
Covariance and contravariance in .NET provide flexibility in assigning and using types that have hierarchical relationships. Covariance allows a method to return a more derived type than specified by the generic parameter. Contravariance allows a method to accept parameters of a more generic type than specified by the generic parameter.โ
Covariance Example:
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // Covariance
Contravariance Example:
Action<object> actObject = obj => Console.WriteLine(obj);
Action<string> actString = actObject; // Contravariance
What .NET engineers should know about covariance and contravariance:
- ๐ผ Junior: Should understand that covariance and contravariance allow for more flexible type assignments in generic interfaces and delegates.
- ๐ Middle: Expected to implement covariance and contravariance in generic interfaces and delegates, understanding how to apply
out
andin
keywords appropriately.โ - ๐ Senior: Should be able to design systems that leverage covariance and contravariance to enhance flexibility and type safety, considering performance implications and potential pitfalls.
๐Resources:
- Covariance and Contravariance (C#) - Microsoft Learn
- Covariance and Contravariance in Generics - .NET - Microsoft Learn
What is Dependency Injection (DI), and why is it essential in .NET?
Dependency Injection (DI) is a design pattern that implements Inversion of Control (IoC). It allows an object's dependencies to be injected at runtime rather than hardcoded, promoting loose coupling, enhancing testability, and improving maintainability. In .NET, DI is a fundamental concept, especially in ASP.NET Core, where it is built into the framework.โ
DI Example:
public interface IMessageService
{
void SendMessage(string message);
}
public class EmailService : IMessageService
{
public void SendMessage(string message)
{
// Send email
}
}
public class Notification
{
private readonly IMessageService _messageService;
public Notification(IMessageService messageService)
{
_messageService = messageService;
}
public void Notify(string message)
{
_messageService.SendMessage(message);
}
}
What .NET engineers should know about DI:
- ๐ผ Junior: Should understand that DI allows for injecting dependencies, promoting loose coupling and easier testing.
- ๐ Middle: Expected to implement DI using .NET's built-in IoC container, register services with appropriate lifetimes, and understand the benefits of DI in application architecture.โ
- ๐ Senior: Should be able to design complex systems utilizing DI, choose appropriate lifetimes for services, and address advanced scenarios like scoped services, circular dependencies, and custom service providers.
๐Resources:
- Dependency injection - .NET | Microsoft Learn
- Dependency injection in ASP.NET Core | Microsoft Learn
- Understanding Dependency Injection in .NET Core - Auth0
Explain immutability in C# and how record
help achieve it.
Immutability refers to the state of an object that cannot be modified after its creation. In C#, immutable objects ensure thread safety and predictability. Records introduced in C# 9.0 provide a concise syntax for creating immutable data models with value-based equality.โ
Example:
// Immutable class prior to C# 9.0
public class Person
{
public string FirstName { get; }
public string LastName { get; }
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
}
// Record in C# 9.0
public record Person(string FirstName, string LastName);
What .NET engineers should know:
- ๐ผ Junior: Understand that immutability means an object's state cannot change after creation and recognize the basic syntax of records.โ
- ๐ Middle: Expected to implement immutable classes and records, understanding the benefits of immutability in multi-threaded scenarios and how records simplify value-based equality.โ
- ๐ Senior: Should be able to design systems that leverage immutability for thread safety and maintainability, utilize records effectively, and understand performance considerations related to immutable objects.โ
๐Resources:
What is Pattern Matching in C#, and where is it used?
Pattern matching in C# enhances the ability to inspect and deconstruct data types, enabling more expressive and concise code. Introduced in C# 7.0 and expanded in later versions, pattern matching allows for checking an object's shape and extracting data from it.โ
Example:
object data = "Hello, World!";
if (data is string message)
{
Console.WriteLine($"String message: {message}");
}
else if (data is int number)
{
Console.WriteLine($"Integer: {number}");
}
else
{
Console.WriteLine("Unknown type");
}
What .NET engineers should know:
In this example, the data
object is tested to determine if it matches specific patternsโa string
or an int
. Based on the pattern it matches, the corresponding code block executes.
- ๐ผ Junior: Understand that pattern matching allows testing an expression's type or value, simplifying type checks and casting operations.โ
- ๐ Middle: Expected to utilize pattern matching in scenarios like type checking, null checks, and deconstructing objects, enhancing code clarity and reducing boilerplate.โ
- ๐ Senior: Should be able to design and implement complex pattern-matching scenarios, including combining multiple patterns and using advanced features like property patterns and positional patterns, to write efficient and maintainable code.
๐Resources:
What are Primary Constructors in C# 12, and how do they differ from traditional constructors?
Primary constructors in C# 12 allow developers to define constructor parameters directly within the class or struct declaration, streamlining initialization and reducing boilerplate code. This feature was previously limited to record types but is now extended to all classes and structs.โ
public readonly struct Distance(double dx, double dy)
{
public readonly double Magnitude { get; } = Math.Sqrt(dx * dx + dy * dy);
public readonly double Direction { get; } = Math.Atan2(dy, dx);
}
What .NET engineers should know:
- ๐ผ Junior: Understand that primary constructors provide a concise syntax for initializing classes and structs, reducing the need for explicit constructor definitions.โ
- ๐ Middle and ๐ Senior: Expected to know how to utilize primary constructors effectively, including in scenarios that enhance code readability and maintainability.
๐Resources: Declare primary constructors for classes and structs
Explain the Collection Expressions introduced in C# 12. How do they simplify collection initialization?
Collection expressions in C# 12 introduce a concise syntax for initializing collections, allowing developers to create lists, arrays, and other collection types using a more readable and succinct format.
var numbers = [1, 2, 3, 4, 5];
What .NET engineers should know:
- ๐ผ Junior: Should recognize that collection expressions provide a shorthand for initializing collections, making the code more concise.โ
- ๐ Middle and ๐ Senior: Expected to understand how to implement collection expressions in various scenarios, including their compatibility with different collection types.โ
What are Inline Arrays in C# 12, and what advantages do they offer?
Inline arrays in C# 12 allow developers to define fixed-size arrays directly within structs, enhancing performance by reducing heap allocations and improving data locality.
public struct Point
{
public int X;
public int Y;
public int[] Coordinates = new int[2];
}
Describe the enhancements made to Pattern Matching in C# 12
C# 12 introduces enhancements to pattern matching, including extended support for list patterns, allowing for more expressive and concise code when working with collections and complex data structures.
var numbers = new[] { 1, 2, 3 };
if (numbers is [1, 2, 3])
{
// Pattern matched
}
What .NET engineers should know:
๐ผ Junior / ๐ Middle /๐ Senior: understand that pattern-matching enhancements provide more straightforward syntax for evaluating data structures.
What is the purpose of the new 'field' keyword introduced in C# 12?
The 'field' keyword in C# 12 allows developers to reference the backing field of an auto-implemented property, providing greater control over property behaviors, especially when implementing custom logic in property accessors.
public string Name
{
get => field;
set => field = value ?? "Default Name";
}
What are the new LINQ methods CountBy
and AggregateBy
in .NET 9, and how do they enhance data querying?
In .NET 9, the introduction of 'CountBy' and 'AggregateBy' methods enriches the LINQ (Language-Integrated Query) capabilities:โ
CountBy: This method allows grouping elements based on a specified key and counting the number of elements in each group. It simplifies scenarios where you must determine the frequency of occurrences within a collection.
Example:
var fruits = new[] { "apple", "banana", "apple", "cherry", "banana", "apple" };
var fruitCounts = fruits.CountBy(fruit => fruit);
// Result: { ("apple", 3), ("banana", 2), ("cherry", 1) }
AggregateBy: This method enables grouping elements by a key and applying an aggregation function to each group. It's useful for performing operations like summing values within each group.
var sales = new[]
{
new { Product = "A", Quantity = 10 },
new { Product = "B", Quantity = 5 },
new { Product = "A", Quantity = 8 }
};
var totalSalesByProduct = sales.AggregateBy(
sale => sale.Product,
0,
(total, sale) => total + sale.Quantity);
// Result: { ("A", 18), ("B", 5) }
What .NET engineers should know:
- ๐ผ Junior: Should know the 'CountBy' and 'AggregateBy' methods in LINQ for grouping and aggregating data.โ
- ๐ Middle /๐ Senior: Expected to utilize 'CountBy' and 'AggregateBy' methods to simplify data querying tasks, understand their syntax and use them properly.
What new cryptographic features have been added in .NET 9?
.NET 9 introduces several advancements in cryptography to enhance security and performance:โ
- One-Shot Hash Methods: The
CryptographicOperations
class now includes aHashData
method, simplifying the process of computing hashes by allowing developers to specify the algorithm directly. This streamlines hashing workflows and reduces potential errors. - KMAC Algorithm Support: .NET 9 introduces classes for the KECCAK Message Authentication Code (KMAC) algorithm, providing robust message authentication and pseudorandom generation capabilities. This includes
Kmac128
,Kmac256
,KmacXof128
, andKmacXof256
, catering to various security requirements - Base64Url Encoding: A new
Base64Url
class has been added, offering optimized methods for encoding and decoding data using the Base64Url scheme, which is URL-safe and commonly used in web applications
How do you implement monitoring and logging mechanisms in C# to track program operation and detect problems?
Implementing custom monitoring and logging in C# involves using the built-in ILogger
interface or creating custom logging providers to record application behavior, which aids in tracking operations and diagnosing issues.โ
What .NET engineers should know:
- ๐ผ Junior: Should be familiar with the basics of the
ILogger
interface and how to use existing logging frameworks likeMicrosoft.Extensions.Logging
.โ - ๐ Middle: Understand the importance of structured logging and implement it for better log analysis.โ Custom logging providers can be created to extend functionality, such as logging into databases or external services.
- ๐ Senior: Designs comprehensive monitoring systems integrating logging with monitoring tools, sets up alerting mechanisms, and ensures logs are scalable and performant.โ
๐Resources:
How does the Equals
method work?
The Equals
method in C# determines whether two object instances are considered equal. The default implementation checks for reference equality but can be overridden to provide value-based equality.โ
What .NET engineers should know:
๐ผ Junior:
- Understand the difference between reference equality (exact memory location) and value equality (same content).โ
- Knows that the default
Equals
method checks for reference equality.โ
๐ Middle:
- Able to override the
Equals
method in custom classes to implement value-based equality checks.โ - Ensures that when
Equals
is overridden,GetHashCode
is also overridden to maintain consistency, primarily when objects are used in collections like dictionaries or hash sets.โ
๐ Senior:
- Designs classes that correctly implement equality contracts, considering inheritance and ensuring equality logic is robust and performant.โ
- Knows about
IEqualityComparer
,IStructuralEquatable
,IComparable
and able to implement them properly.
๐Resources:
How do we dynamically use type indexing in C# to create objects and call methods at runtime?
Type indexing refers to dynamically accessing types, creating instances, and invoking methods at runtime using reflection. This technique is beneficial when the types or methods are unknown at compile time.
What .NET engineers should know:
๐ผ Junior:
- Understand the concept of reflection in C#, which allows the inspection of metadata about types at runtime.โ
- Know how to create instances of types dynamically using
Activator.CreateInstance
.โ
Example:
Type type = Type.GetType("Namespace.ClassName");
object instance = Activator.CreateInstance(type);
- Know how to invoke methods on dynamically created instances using
MethodInfo
.โ
Example:
MethodInfo method = type.GetMethod("MethodName");
method.Invoke(instance, null);
๐ Middle:
- Be capable of invoking methods that require parameters by passing an object array to
method.Invoke
.โ - Understand the performance implications of reflection and explore alternatives like expression trees or delegates for improved efficiency.
๐ Senior: Utilize reflection to comprehensively inspect assemblies, modules, and types. Implement dynamic proxies or use libraries like Castle DynamicProxy to intercept method calls and add cross-cutting concerns such as logging or caching.
๐Resources:
Dynamically Loading and Using Types
What are Dynamic Types in C#?
Dynamic types in C# allow developers to bypass compile-time type checking using the dynamic
keyword, with type resolution occurring at runtime. This enables flexible coding scenarios, such as interacting with dynamic languages or COM objects, but introduces performance overhead due to runtime binding. The Dynamic Language Runtime (DLR), a component of .NET, underpins dynamic
by providing the infrastructure for dynamic dispatch, caching, and interoperability with languages like Python or JavaScript.
dynamic obj = "Hello";
Console.WriteLine(obj.Length); // Outputs: 5
obj = 42;
Console.WriteLine(obj + 10); // Outputs: 52
In this example, obj is typed as dynamic, allowing it to hold a string and then an integer, with operations resolved at runtime via the DLR.
What .NET engineers should know:
- ๐ผ Junior: Should understand that
dynamic
allows flexible type usage without compile-time checks, but itโs slower than static types and is powered by the DLR for runtime behavior. - ๐ Middle: Expected to use
dynamic
for scenarios like COM interop or dynamic APIs, understand the DLRโs role in enabling runtime binding, and recognize the performance trade-offs compared to static typing. - ๐ Senior: Should evaluate when
dynamic
is necessary (e.g., for dynamic language integration), optimize its use by minimizing runtime overhead, and leverage the DLRโs capabilities for advanced interoperability while preferring static types for performance
What is the Dynamic Language Runtime (DLR)?

The Dynamic Language Runtime (DLR) is a set of libraries in .NET that provides infrastructure for dynamic typing and execution, enabling features like the dynamic
keyword in C# and interoperability with dynamic languages such as Python and JavaScript. Built on top of the Common Language Runtime (CLR), the DLR handles runtime type resolution, dynamic dispatch, and caching of dynamic operations to improve performance. It supports scripting, COM interop, and dynamic code generation by providing a flexible binding system that resolves method calls and property access at runtime.
For example, the DLR enables C# to call methods on a Python object dynamically:
dynamic pythonObj = GetPythonObject(); // Hypothetical Python object
pythonObj.SomeMethod(); // DLR resolves this at runtime
The DLRโs caching mechanisms reduce the overhead of repeated dynamic calls, making it efficient for scenarios requiring dynamic behavior.
What .NET engineers should know about DLR:
- ๐ผ Junior: Should know that the DLR is a .NET component that makes dynamic types work by figuring out types and operations at runtime, unlike regular C# code that checks types during compilation.
- ๐ Middle: Expected to understand the DLRโs role in enabling dynamic typing, its use in scenarios like COM interop or scripting, and how it improves performance with caching compared to raw reflection.
- ๐ Senior: Should leverage the DLR for advanced use cases like integrating C# with dynamic languages or building extensible systems, optimize dynamic operations using DLR caching, and balance its flexibility against static typingโs performance benefits.
๐ Resources:
What are symbolic links in .NET? How do you create symbolic links in .NET?
Symbolic links are advanced shortcuts. When you create a symbolic link to an individual file or folder, Windows will perceive it as the same file or folderโeven though it's just a link pointing at the file or folder.
For example, let's say you have a program that needs its files at C:\Program. You'd like to store this directory at D:\Stuff, but the program requires that its files be at C:\Program. You could move the original directory from C:\Program to D:\Stuff
, then create a symbolic link at C:\Program pointing to D:\Stuff. When you relaunch the program, it will try to access its directory at C:\Program. Windows will automatically redirect it to D:\Stuff, and everything will work as if it were in C:\Program.
In .NET, symbolic linksโpointers that reference other files or directoriesโcan be created using the CreateSymbolicLink
methods provided by the File
and Directory
classes. These methods establish links that, when accessed, redirect to the target file or directory.
Creating a File Symbolic Link:
using System.IO;
string linkPath = @"C:\path\to\symbolicLink.txt";
string targetPath = @"C:\path\to\targetFile.txt";
File.CreateSymbolicLink(linkPath, targetPath);
To create a symbolic link to a directory
using System.IO;
string linkPath = @"C:\path\to\symbolicDirectory";
string targetPath = @"C:\path\to\targetDirectory";
Directory.CreateSymbolicLink(linkPath, targetPath);
๐ Resources:
- Create symbolic links in .NET
- File.CreateSymbolicLink Method
- Directory.CreateSymbolicLink Method
- Create symbolic links
Explain the concept of nullable reference types introduced in C# 8.0.
Nullable reference types, introduced in C# 8.0, enhance code safety by allowing developers to explicitly specify whether a reference type variable may be null, helping to catch potential null-related issues at compile time and reducing runtime NullReferenceException
errors. By default, reference types in C# can be null, leading to unexpected errors if not handled. With nullable reference types enabled, developers can annotate variables to indicate nullability, and the compiler uses static flow analysis to enforce safe usage. Flow analysis improvements in later C# versions (e.g., C# 9.0+) provide more precise warnings, such as detecting nullability in complex control flows, and nullable annotation attributes like AllowNull
and DisallowNull
offer finer control over nullability contracts.
- Non-nullable reference types are declared without annotation, indicating they should not be null. The compiler issues warnings for potential null assignments or dereferences.
string nonNullableString = "Hello";
nonNullableString = null; // Compiler warning: Assigning null to non-nullable type
Nullable reference types are declared with a ?
suffix, indicating they can be null. The compiler enforces null checks before dereferencing.
string? nullableString = "Hello";
nullableString = null; // No warning
if (nullableString != null)
{
Console.WriteLine(nullableString.Length); // Safe dereference
}
Nullable Annotation Attributes: AllowNull
and DisallowNull
refine nullability for advanced scenarios. For example, AllowNull
permits null input for a non-nullable property while DisallowNull
ensuring a nullable parameter cannot be null.
public class Example
{
[AllowNull]
public string NonNullableString { get; set; } = "Default";
public void Process([DisallowNull] string? input)
{
Console.WriteLine(input.Length);
}
}
Enabling Nullable Reference Types:
To enable this feature, add the following to your projectโs .csproj file:
<Nullable>enable</Nullable>
What .NET engineers should know:
- ๐ผ Junior: Should understand the purpose of nullable reference types, how to declare variables as nullable
string?
or non-nullablestring
, and why this helps avoid null errors. - ๐ Middle: Expected to implement nullable reference types in projects, refactor legacy code to use them, and apply attributes like
AllowNull
,DisallowNull
, orNotNull
to clarify nullability in complex APIs. - ๐ Senior: Should design null-safe systems leveraging advanced flow analysis in C# 9.0+ for precise null checking, use attributes like
AllowNull
andDisallowNull
to enforce robust contracts and guide teams on adopting nullable reference types to improve code reliability and maintainability.
๐ Resources:
What are the benefits and Use Cases of the init
accessor introduced in C# 9.0?
The init
accessor, introduced in C# 9.0, allows properties to be assigned during object initialization but makes them immutable, preventing modifications after construction. This feature simplifies the creation of immutable types by enabling concise object initialization while ensuring data integrity. The init
accessor pairs seamlessly with record types, also introduced in C# 9.0, to support immutable object initialization, promoting robust immutable design patterns where objects behave like values with fixed states after creation.
- This feature enables properties to be set during object creation using object initializers, ensuring immutability post-initialization without requiring complex constructor logic.
- Provides clear, readable object initialization, improving code maintainability compared to constructor-only approaches.
- Enhances record types by allowing flexible initialization of immutable properties, supporting value-based equality and immutable data models critical for reliable software design.
Example
public class Product
{
public string Name { get; init; }
public decimal Price { get; init; }
}
var product = new Product
{
Name = "Laptop",
Price = 999.99m
};
// product.Price = 899.99m; // Compile-time error: Property is init-only
In this example, the Product
class uses init
accessors, allowing Name
and Price
to be set during initialization but preventing changes afterward, ensuring immutability.
Example with Record:
public record ProductRecord(string Name, decimal Price);
var productRecord = new ProductRecord("Laptop", 999.99m);
// productRecord.Price = 899.99m; // Compile-time error: Record properties are init-only
Records use init
accessors by default, combining immutable initialization with value-based equality for concise, robust data models.
Use Cases:
- Immutable Data Models are ideal for defining data models, such as configuration settings or data transfer objects (DTOs), where property values should remain fixed after creation to prevent unintended changes.
- Record Types: This class leverages
init
accessors to initialize immutable records, ensuring thread-safe, predictable behavior in domains like financial systems or API payloads. - Functional Programming Patterns: This pattern supports immutable object design, aligning with functional paradigms where state changes are minimized for better reliability.
What .NET engineers should know:
- ๐ผ Junior: Should understand how to declare init accessors and use them to create immutable properties, recognizing their role in preventing changes after object creation.
- ๐ Middle: Expected to apply init accessors in projects, especially with records, to build immutable data models and identify scenarios where immutability improves code safety and clarity.
- ๐ Senior: Should design systems leveraging init accessors and records for immutable, thread-safe designs, guide teams on their use in functional and domain-driven patterns, and optimize initialization workflows for maintainability and performance.
๐ Resources: