Components of the .NET Architecture
As we mentioned earlier, there is a lot to the .NET Framework. In this section, we identify the individual components and describe their features and how they fit into the overall picture.
The heart of the .NET Framework is the CLR. Similar in concept to the Java Virtual Machine, it is a runtime environment that executes MSIL code. Unlike the Java environment, which is the concept of one language for all purposes, the .NET platform supports multiple programming languages through the use of the Common Language Specification, which defines the output required of compilers that want to target the CLR.
Because all code targeted at the .NET platform runs with the CLR environment, it is referred to as managed code.This simply means that the execution of the code and its behavior is managed by the CLR.The metadata available with managed code contains the information required to allow the CLR to manage its safe execution. By safe execution we mean memory and security management, type safety, and interlanguage interoperability. Unmanaged code can write to areas of memory it does not own, execute instructions at arbitrary locations in memory, and exhibit any number of other bad behaviors that cannot be managed or prevented by the CLR. Most of the applications running on Windows today are unmanaged.
The .NET intermediate language, MSIL, is defined in the Common Language Specification. It is an amalgam of a low-level language similar in many ways to a machine language and a higher object language.You can write applications directly in MSIL,much as you can write directly in assembly language.Thankfully, this is not necessary for most purposes.
Common Type System
.NET applications, regardless of their source languages all share a common type system.What this means is that you no longer have to worry when doing development in multiple languages about how a data type declared in one language needs to be declared in another.Any .NET type has the same attributes regardless of the language it is used in. Furthermore, all .NET data types are objects, derived from System.Object.
Because all data types derive from a common base class, they all share some basic functionality, for example the ability to be converted to a string, serialized, or stored in a collection.
.NET Base Class Library (BCL)
If I could have bought a library that offered everything the .NET Base Class Library offers when I started programming, a year's salary would have seemed reasonable-there really is that much to it. Almost everything in the .NET environment is contained within the BCL.
Let's look at a "Hello World" example:
public static void Main()
The only function contained in this simple program is a call to the WriteLine method of the Console class.What is really unique about the .NET environment is that .NET languages don't have to implement even the most basic functions; they are available in the BCL. Because all .NET languages share the same common set of libraries, the code being executed by your C# program is the same code being executed by a program written in another language.This means
that all languages that target the .NET environment essentially share the same capabilities, except they have different syntax. Some people will wonder why we even have different languages if they all have the same capabilities. A few reasons immediately spring to mind:
Programmers don't like change.
Programmers usually have a favorite language.
Programmers don't like change.
Imagine if Microsoft had come out with all the good things in .NET, but said that in order to use it, we all had to learn a new language. Lots of people might have never even given it an honest look unless forced by their employers.Making it available for all languages makes it seem less like the chore of learning a new language and more like the excitement of receiving a new library with tens of thousands of functions that will make your life as a developer easier.
Assemblies are the means of packaging and deploying applications and components in .NET. Just like a compiled application or component today, assemblies can be made up of either single or multiple files. An assembly contains metadata information (covered in the next section), which is used by the CLR for everything from type checking and security to actually invoking the components methods. All of this means that you don't need to register .NET components,
unlike COM objects.
Metadata is the feature that lets the CLR know the details about a particular component.The metadata for an object is persisted at compile time and then queried at runtime so that the CLR knows how to instantiate objects, call their methods, and access their properties.Through a process called reflection, an application can interrogate this metadata and learn what an object exposes.This is similar to the way IDispatch and type libraries work in COM.
Unlike COM, where the information about a component can be found in type libraries and the Registry, where it is only associated with the actual component, .NET metadata is stored within the component itself in a binary format packaged inside the assembly.The metadata contains a declaration for every type and a declaration, including names and types, for all of its members (methods, fields, properties, and events). For every method implemented by the component, the metadata contains information that the loader uses to locate the method body. It is also possible (but not required) for the creator of a class type to associate help text and comments with a method or parameter in the metadata, similar to the way that information can be associated with a component using information within the IDL in the COM world.
Besides the low-level information described in this section, a component also includes information regarding its version and any culture information specific to the component.The culture information can be queried at runtime and used in developing localized applications. Look at the System.Reflection.AssemblyName class as a place to get started, and check out the CultureInfo class to see how extensive the culture support of .NET components can be.You can also use reflection to determine a components version, which might be useful if your application is dynamically loading components and needs to make adjustments for different versions.
Assemblies and Modules
.NET applications are deployed as assemblies, which can be a single executable or a collection of components.When you create a .NET application, you are actually creating an assembly, which contains a manifest that describes the assembly.
This manifest data contains the assembly name, its versioning information, any assemblies referenced by this assembly and their versions, a listing of types in the assembly, security permissions, its product information (company, trademark, and so on), and any custom attribute.
An assembly that is shared between multiple applications also has a shared name (also known as a strong name).This is a key pair containing a globally unique name (think GUID from COM) as well as an encrypted digital signature to prevent tampering.This information is optional and may not be in a component's manifest if it was not intended as a shared component.
Creating .NET modules that do not contain assembly manifest data is also possible.These modules can then be added to an assembly, by including it in the Visual Studio project. An example of why you might want to do this would be if you had a component that was logically divided into several subcomponents that would be best distributed and versioned as a single unit.
The assembly cache is a directory normally found in the \WinNT\Assembly directory. When an assembly is installed on the machine, it can be merged into the assembly cache, depending upon the installation author or the source of the assembly.The assembly cache has two separate caches: a global assembly cache and a transient assembly cache.When assemblies are downloaded to the local machine using Internet Explorer, the assembly is automatically installed in the transient assembly cache. Keeping these assemblies separated prevents a downloaded component from impacting the operation of an installed application.
Now for what may be a great feature that you won't think of until your project is finished.The assembly cache will hold multiple versions of an assembly, and if your installation programs are written correctly, they cannot overwrite a previous version of an assembly that may be needed by another application.You read that right, the .NET Framework is making a solid effort to banish DLL Hell.
Just to clarify what this means, the assembly cache can contain multiple versions of a component, as an example, we'll say we've installed versions 1.0 and 1.1 of MyComponent.dll on a system. If an application was built and tested using Version 1.0 of MyComponent.dll, the CLR will see this when it reads the application's metadata and will load Version 1.0 of MyComponent.dll, even though a later version of the assembly exists in the cache.The application will continue to function normally because the code that it is executing is the same code that it was built and tested with.Thanks to this feature, you also don't have to maintain compatibility with earlier versions of your components.This feature alone is enough to make the .NET architecture great.
Reflection is the means by which .NET applications can access an assembly's metadata information and discover its methods and data types at runtime.You can also dynamically invoke methods and use type information through late binding through the Reflection API.
The System.Type class is the core of the reflection system. System.Type is an abstract class that is used to represent a Common Type System type. It includes methods that allow you to determine the type's name, what module it is contained in, and its namespace, as well as if it is a value or reference type.
For example, using the System.Reflection.Assembly class you can retrieve all of the types in an assembly, and all of the modules contained in the assembly.To invoke a method of a class loaded at runtime, you would use a combination of the Activator class to create an instance of the type you had obtained through the Assembly class.Then you can use the type's GetMethod method to create a MethodInfo object by specifying the method name that you wish to invoke. At this point, you can use the MethodInfo object's Invoke method, passing it the instance of the type you created with the Activator class. It sounds a lot like some of the nasty bits of COM programming, but the Reflection API genuinely makes it a lot easier.
Just In Time Compilation
The .NET CLR utilizes Just In Time (JIT) compilation technology to convert the IL code back to a platform/device-specific code. In .NET, you currently have three types of JIT compilers:
Pre-JIT This JIT compiles an assembly's entire code into native code at one stretch.You would normally use this at installation time.
Econo-JIT You would use this JIT on devices with limited resources. It compiles the IL code bit-by-bit, freeing resources used by the cached native code when required.
Normal JIT The default JIT compiles code only as it is called and places the resulting native code in the cache. In essence, the purpose of a JIT compiler is to bring higher performance to interpreted code by placing the compiled native code in a cache, so that when the next call is made to the same method/procedure, the cached code is executed, resulting in an increase in application speed.
Memory management is one of those housekeeping duties that takes a lot of programming time away from developing new code while you track down memory leaks. A day spent hunting for an elusive memory problem usually isn't a productive day.
.NET hopes to do away with all of that within the managed environment with the garbage collection system. Garbage collection runs when your application is apparently out of free memory, or when it is implicitly called but its exact time of execution cannot be determined. Let's examine how the system works. When your application requests more memory, and the memory allocator reports that there is no more memory on the managed heap, garbage collection is called.The garbage collector starts by assuming everything in memory is trash that can be freed. It then walks though your application's memory, building a graph of all memory that is currently referenced by the application. Once it has a complete graph, it compacts the heap by moving all the memory that is genuinely in use together at the start of the free memory heap. After this is complete, it moves the pointer that the memory allocator uses to determine where to start allocating memory from the top of this new heap. It also updates all of your application's references to point to their new locations in memory.This approach is commonly called a mark and sweep implementation. The exception to this is with individual objects over 20,000 bytes.Very large objects are allocated from a different heap, and when this heap is garbage collected, they are not moved, because moving memory in this size chunks would have an adverse effect on application performance.
As you can see, garbage collection involves a lot of work, and it does take some time.A number of performance optimizations involved in the .NET garbage collection mechanism make it much more than the simple description given here.
Normally you will just let the CLR take care of running garbage collection when it is required. However, at times you may want to force the garbage collector to run, perhaps before starting an operation that is going to require a large amount of memory.To do this, just call GC.Collect(). And if you want to report on your memory use at various points during your application's execution to help you determine when might be a good time to force collection, you can use GC.GetTotalMemory(bool forceFullCollection).
As you can probably guess, the parameter forceFullCollection determines if garbage collection is run before returning the amount of memory in use.