In the beginning, there was Java. But then, around 2000, the beta version of .NET arrived, arguably due to pushback from a 1997 lawsuit filed by Sun Microsystems.
Even though the code written using an early implementation of C# and Java looked almost identical at first glance, several things made .NET truly unique.
Although there is a lot of material on .NET online, much of it has issues, such as:
It’s not focused purely on .NET (specifically the .NET framework).
Even though it covers advanced topics, it mostly covers basic topics from a coding perspective.
In this blog, we will focus on core .NET framework-related questions, including some related to recent .NET updates, up to release 9. We will try our best not to focus on language-specific details.
To ensure the questions covered in this blog are unique, we have specifically chosen questions requiring at least an intermediate-level programming skill level and a minimum of three years of programming experience. While there are no language-specific questions, you need a basic understanding of at least one programming language in .NET.
So, are you excited to learn? Let‘s get going.
Let’s first understand what a module is and the difference between a managed and an unmanaged module.
When we compile source code files in Microsoft .NET, we get a module, whether it’s in any of the numerous supported languages, including C++, C# (pronounced as C Sharp), Iron Python, or Visual Basic.
A managed module is, quite simply, a standard Windows 32 or Windows 64-bit portable executable file. The only difference is that it needs the CLR (common language runtime) for its execution.
A managed module has four parts: metadata, portable executable header, CLR header, and IL (intermediate language) code.
Let’s briefly discuss each of these next.
The file header for a standard Windows file is based on a format commonly called the Common Object File Format (COFF). In the case of PE32, the file can be executed on either 32- or 64-bit Windows. The PE32+ header in a file implies that the file can only be executed on 64-bit Windows.
Some of the items inside the header include:
Type of file, e.g., if the file is a GUI, CUI, or a DLL
Time stamp demonstrating build time
The CLR header contains information that makes the managed module what it is. For example, it can contain the CLR version, some flags, and modules entry point method (e.g., main). Additionally, it can contain other items, such as identifying the resources, flags, and the location of the module’s metadata.
One thing that sets a managed module apart is the metadata section, which hosts metadata tables.
These are primarily of two main types: one that has the type description/members defined in or referenced in the code.
This is the actual code that the compiler has produced from the source code and is converted into native CPU instructions when the code is executed.
By default, the Microsoft compiler creates code called safe code. This means that it is verifiably safe because it follows the rules of managed execution, such as rules related to garbage collection by the .NET runtime. Specifically, verification checks code safety. These include checking the number and type matching parameters in methods and return types, among other things.
However, unsafe code can bypass these checks and directly access addresses. So, it is a double-edged sword. On the one hand, it allows code to go and bypass rules; if used carelessly, it can result in some of the same problems that Microsoft .NET has been designed to solve.
Because of the risks involved with using unsafe, it needs to be marked with a particular keyword or with the help of a compiler switch.
The way this works is closely linked to the JIT (just-in-time) compiler, which converts IL to native code.
For unsafe methods, the JIT compiler has the correct permissions for execution. Only in that case, the JIT compiler will allow for compilation and execution of the unsafe code.
Yes, it is possible to do that in different ways.
First, a specific tool inside the .NET framework is called Ngen.exe. Since this code is pre-compiled, there’s no need to do the same at execution time.
Then there is .NET native, which is a newer method.
Finally, there’s cross-compilation, which allows for creating executable code for a platform other than the one on which the compiler is running.
There are two specific benefits to using native code. First, it can improve initial startup time. Second, it can reduce the application’s working set because having a natively compiled piece of code can be shared between processes.
Whenever we create a program, it requires various resources. These can be memory-related (e.g., buffers), screen-based (e.g., screen space), network-based (e.g., connections), and database-based (e.g., connections).
If we want to use them, then they must be allocated in the form of a type. The way that works is there is a sequence of steps that take place accordingly:
First, memory is allocated for the type.
This memory is set to an initial state to make the resource usable.
This is then used by accessing the type’s members.
The resource is signed off for tear down.
The garbage collector typically frees the memory at the end.
The problem being solved here is that typical programmers, such as those with a C/C++ background, are used to doing all this independently. This often results in memory leaks when allocated memory is no longer deallocated.
However, these practices can cause other problems. Sometimes, programmers use memory after releasing it, which can easily lead to memory corruption bugs and even security issues. Given that we avoid unsafe code, the application is almost guaranteed to be safe against memory corruption.
The idea of the managed heap is not only to eradicate these errors but also to give a much-simplified programming model, which only has the following steps:
Allocate
Initialize
Use
In other words, there is no way or need to clean things up. Still, that doesn’t mean we shall never have to clean up ourselves. In certain cases, if we consume types that need special cleaning, we can clean things up as soon as possible without waiting for the garbage collector. All we have to do in this case is call a special method, e.g., dispose. However, as discussed, that is a rare scenario rather than the norm.
Yes, .NET has another performance improvement mechanism considering the objects' size. For large objects, i.e., 85,000 bytes or larger, the CLR has a different treatment:
These objects are allocated in a different address space.
Currently, as of 2024, the garbage collector doesn't compact them as this would be a considerably expensive operation.
Large objects are considered a part of the second generation by default. Considering that the .NET garbage collector is a generational garbage collector with generations starting from 0, then 1, then 2, these large objects, e.g., XML files or JSONs or byte arrays, are handled this way. These are part of the LOH (Large Object Heap).
There are two garbage collection (GC) modes that the CLR uses:
The workstation mode fine-tunes purely for applications more suitable for client mode. Since it is focused on applications, it will provide minimal latency to application threads.
In server mode, garbage collection optimizes itself to ensure better throughput and more efficient utilization of available resources. The GC assumes no other applications are executing on the machine.
This also involves splitting GC across CPUs so that there is one section of GC per CPU. More specifically, there is one thread per CPU, and a thread collects one section.
This is interesting because it involves advanced concepts of how Microsoft Windows and COM (Component Object Model) interact at the fundamental level. Before discussing CLR hosting, we must examine how Microsoft executes the .NET runtime on Microsoft Windows.
Since the .NET framework runs on Windows, it will be implemented according to the same rules. By default, CLR has been created as a COM server hosted inside a DLL. That means a standard interface is identified using unique strings called GUIDs (Globally Unique Identifiers) assigned to it.
Typically, this would have been instantiated in COM using a call to a Windows function called CoCreateInstance. However, for CLR, the way it works is there is a need to call another function instead of CLRCreateInstance, which is declared in a header file Metahost.h
. Now, this function is implemented in a DLL called the shim. The job of the shim (MSCorEE.dll
) is to determine the actual version of CLR to be created. It, however, does not contain the CLR COM server inside it.
Whenever a managed executable starts, the shim checks it to understand which version of CLR it was built and tested for. Multiple installed CLR versions can exist.
To host CLR, we simply have to use CLRCreateInstance and then start any managed app by calling:
AppDomain.CurrentDomain.ExecuteAssembly(assemblyName);
CLR hosting can be useful because it allows any application to offer those features and can be written at least partially in managed code. Some of the benefits include:
You can write your code in a language of your choice.
Code is pre-compiled for speed rather than being interpreted at runtime.
Execution is in a secure sandbox.
Garbage collection helps in stopping memory leaks.
Hosts can simply reuse existing technologies for development.
There are two main reasons for using threads:
Responsiveness: First, and typically for GUI applications running at the client end, one key use of threads is to remove wait time if the main thread is busy.
Performance: This use case works for both the client and server. Windows schedules one thread per CPU, so threads can easily increase application performance.
We would also like to note that although Microsoft initially attempted to create a different type of thread for the CLR from Windows threads, the idea was abandoned after 2005. They are identical, but some deprecated APIs may be inside the framework.
Creating and destroying a thread can be a taxing operation regarding time and memory. It can also affect system performance because the OS must switch the context between threads.
CLR has been designed to help improve this situation by performing thread pool management for its own managed code.
So, it works because each CLR has a single shared thread pool. In the case of multiple CLRs loading within one process, each CLR has a separate pool.
Three patterns for performing asynchronous operations are available, namely Task-based Asynchronous Pattern (TAP), Event-based Asynchronous Pattern (EAP), and Asynchronous Programming Model (APM).
The latest version is .NET 9, a successor to .NET 8. This particular version focuses on cloud-native apps and improving performance. It includes strong cloud-native fundamentals, including runtime performance and application monitoring.
In the previous release, .NET 8, the focus on AI was already expanded beyond ML.NET to TensorPrimitives for .NET, in addition to connecting with ChatGPT and Enterprise date with Azure OpenAI and cognitive search .NET sample.
We have presented various top interview questions for intermediate and advanced .NET programmers. If you want to learn more about .NET, the Educative platform has numerous resources ranging from the basics, allowing you to learn about .NET, C#, and advanced C#.
Check out some of these courses below!
Free Resources