.NET as a Component Technology

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.

Simplified Component Development

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.

The .NET base classes

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.

Component inheritance

.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.

Component visibility

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.

Attribute-based programming

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.

Component-oriented security

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.

Simplified Component Deployment

.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.

Simplified Object Life Cycle Management

.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.

Nondeterministic Finalization

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 Windows Interoperability

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset