To simplify component development, one of the goals set for the .NET framework was to improve COM deficiencies. Some of these deficiencies, such as awkward concurrency management via apartments, were inherited with COM itself. Other deficiencies occur as a result of error-prone development and deployment phases.
Examples include memory and resource leaks resulting from reference count defects, fragile registration, the need for developer-provided proxy stubs pairs, and having interface and type definition in IDL files separate from the code. Frameworks such as ATL do provide automation of some of the required implementation plumbing, such as class factories and registration, but they introduce their own complexity.
.NET is designed to not only improve these deficiencies, but also maintain the core COM concepts that have proven themselves as core principles of component-oriented development.
.NET provides you fundamental component-oriented development principles, such as binary compatibility between client and component, separation of interface from implementation, object location transparency, concurrency management, security, and language independence. A comprehensive discussion of .NET as a component technology merits a book in its own right and is beyond the scope of this appendix. However, the following sections describe the main characteristics of .NET as a component technology.
Compared to COM, .NET might seem to be missing many things you take for granted as part of developing components. However, in essence, the missing elements are actually present in .NET, although in a different fashion:
There is no canonical base interface (such as
IUnknown
) that all components derive from.
Instead, all components derive from the
System.Object
class. Every
.NET object is therefore polymorphic with
System.Object
.
There are no class factories. In .NET, the runtime resolves a type declaration to the assembly containing it and the exact class or struct within the assembly.
There is no reference counting of objects. .NET has a sophisticated garbage collection mechanism that detects when an object is no longer used by clients. Then the garbage collector destroys the object.
There are no IDL files or type libraries describing your interfaces and custom types. Instead, you put those definitions in your source code. The compiler is responsible for embedding the type definitions in a special format in your assembly called metadata.
There are no GUIDs. Scoping the types with the namespace and assembly name provides uniqueness of type (class or interface). When sharing an assembly between clients, the assembly must contain a strong name—a unique binary blob generated with an encryption key. Globally unique identifiers do exist in essence, but you do not have to manage them anymore.
There are no apartments. By default, every .NET component executes in a free-threaded environment and you are responsible for synchronizing access to your components. Providing synchronization is done by either relying on .NET synchronization locks or using COM+ activities.
.NET has a superb development environment and semantics, the product of years of observing how developers use COM and the hurdles they faced.
As demonstrated in Example C-1, a hard-to-learn component development framework such as ATL is not required to build binary managed components. .NET takes care of all the underlying plumbing for you. To help you develop your business logic faster, .NET also provides you with more than 3,500 base classes, available in similar form for all languages. The base classes are easy to learn and apply. You can use the base classes as is, or derive from them to extend and specialize their behavior.
.NET enforces strict
inheritance semantics and inheritance
conflicts resolution. .NET does not allow multiple inheritance of
implementation. You can only derive from one concrete class. You can,
however, derive from as many interfaces as you like. When you
override a virtual function implementation in a base class, you must
declare your intent explicitly. For example, if you want to override
it, you should use the override
reserved word.
While
developing a set of interoperating
components, you often have components that are intended only for
private use and should not be shared with your clients. Under COM,
there is no easy way of guaranteeing that the components are only
used privately. The client can always hunt through the Registry, find
the CLSID of your private component, and use it. In .NET, you can
simply use the
internal
keyword on the class definition
(instead of public
, as in Example C-1).
The runtime denies access to your component for any caller outside
your assembly.
When developing components, you can use attributes to declare your component needs, instead of coding them. Using attributes to declare component needs is similar to the way COM developers declare the threading model attribute of their components. .NET provides you with numerous attributes, allowing you to focus on your domain problem at hand (COM+ services are accessed via attributes). You can also define your own attributes or extend existing ones.
The classic Windows NT security model is based on what a given user is allowed to do. This model has evolved in a time when COM was in its infancy and applications were usually standalone, monolithic chunks of code. In today’s highly distributed, component-oriented environment, there is a need for a security model based on what a given piece of code—a component—is allowed to do, and not only on what its caller is allowed to do.
.NET allows you to configure permissions for a piece of code and provide an evidence to prove that it has the right credentials to access a resource or perform sensitive work. Evidence-based security is tightly related to the component’s origin. System administrators can decide that they trust all code that came from a particular vendor, but distrust everything else, from downloaded components to malicious attacks. A component can also demand that a permission check be performed to verify that all callers in its call chain have the right permissions before it proceeds to do its work.
This model complements COM+ role-based security and call authentication. It provides the application administrator with granular control over not only what the users are allowed to do, but also what the components are allowed to do. .NET has its own role-based security model, but it is not as granular or user friendly as COM+ role-based security.
.NET does not rely on the Registry for anything that has to do with your components. In fact, installing a .NET component is as simple as copying it to the directory of the application using it. .NET maintains tight version control, enabling side-by-side execution of new and old versions of the same component on the same machine. The net result is zero-impact install—by default, you cannot harm another application by installing yours, thus ending the predicament known as DLL Hell. The .NET motto is: it just works. If you want to install components to be shared by multiple applications, you can install them in the Global Assembly Cache (GAC). If the GAC already contains a previous version of your assembly, it keeps it for use by clients that were built against the old version. You can purge old versions as well, but that is not the default.
.NET does not use reference counting to manage an object’s life cycle. Instead, .NET keeps track of accessible paths in your code to the object. As long as any client has a reference to an object, it is considered reachable. Reachable objects are kept alive. Unreachable objects are considered garbage, and therefore destroying them harms no one. One of the crucial CLR entities is the garbage collector . The garbage collector periodically traverses the list of existing objects. Using a sophisticated pointing schema, it detects unreachable objects and releases the memory allocated to these objects. Consequently, clients do not have to increment or decrement a reference count on the objects they create.
In
COM, the object knows that it is no longer
required by its clients when its reference count goes down to zero.
The object then performs cleanup and destroys itself by calling
delete
this;
. The ATL framework
even calls a method on your object called FinalRelease( )
, letting you handle the object cleanup.
In .NET, unlike COM, the object itself is never told when it is
deemed as garbage. If the object has specific cleanup to do, it
should implement a method called Finalize( )
. The
garbage collector calls Finalize( )
just before
destroying the object. Finalize( )
is your .NET
component’s destructor. In fact, even if you implement a
destructor (such as the one in Example C-1), the
compiler will convert it to a Finalize()
method.
However, simplifying the object lifecycle comes with a cost in system
scalability and throughput. If the object holds on to expensive
resources, such as files or database connections, those resources are
released only when Finalize( )
is called. It is
called at an undetermined point in the future, usually when the
process hosting your component is out of memory. In theory, releasing
the expensive resources the object holds may never happen, and thus
severely hamper system scalability and throughput.
There are two solutions to the problems arising from nondeterministic
finalization. The first solution is to implement methods on your
object that allow the client to explicitly order cleanup of expensive
resources the object holds. If the object holds onto resources that
can be reallocated, then the object should expose methods such as
Open( )
and Close( )
.
An object encapsulating a file is a good example. The client calls
Close( )
on the object, allowing the object to
release the file. If the client wants to access the file again, it
calls Open( )
without re-creating the object. The
more common case is when disposing of the resources amounts to
destroying the object. In that case, the object should implement a
method called Dispose( )
. When a client calls
Dispose( )
, the object should dispose of all its
expensive recourses, and the client should not try to access the
object again. The problem with both Close( )
and
Dispose( )
is that they make sharing the object
between clients much more complicated than COM’s reference
counts. The clients have to coordinate which one of them is
responsible for calling Close( )
or
Dispose( )
and when Dispose( )
should be called; thus, the clients are coupled to one another.
The second solution to nondeterministic finalization is to use COM+ JITA, as explained in Chapter 10.
COM and .NET are fully
interoperable. Any COM client can call your managed objects, and any
COM object is accessible to a managed client. To export your .NET
components to COM, use the TlbExp.exe
utility,
also available as a command from the Tools menu. The utility
generates a type library that COM clients use to CoCreate managed
types and interfaces. You can use various attributes on your managed
class to direct the export process, such as providing a CLSID and
IID.
To import an existing COM object to .NET (by far the most common
scenario), use the TlbImp.exe
utility. The
utility generates a managed wrapper class, which your managed client
uses. The wrapper manages the reference count on the actual COM
object. When the wrapper class is garbage collected, the wrapper
releases the COM object it wraps. You can also import a COM object
from within the Visual Studio.NET environment by selecting the COM
object from the project reference dialog (which makes Visual
Studio.NET call TlbImp
for you).
.NET has support for invoking native Win32 API calls, or any DLL exported functions, by importing the method signatures to the managed environment.