NGWS SDK Documentation  

This is preliminary documentation and subject to change.
To comment on this topic, please send us email at ngwssdk@microsoft.com. Thanks!

Aggregation, Containment and Inheritance

The COM reusability is accomplished through containment and aggregation. Managed objects can participate in both models as the inner object in the relationship as described below. When it’s desirable for the managed object to be the outer object, the NGWS runtime provides inheritance (see section 0).

The only requirement for managed object to support aggregation or containment is that the type must be public and must have a public default constructor. Meeting both these requirements simply makes the object COM creatable, which is necessary for both containment and aggregation of the object.

Containing Managed Objects

Containment is the simpler of the two reuse models and is supported by managed objects. Containment is typically used when the outer object needs to modify the behavior of the inner object. To do so, the outer object simply creates an instance of the inner object during construction and delegate calls to the inner object as necessary. The outer object can choose which calls to delegate and which calls to handle it self. There are no special requirements of the runtime in order for objects to support containment.

Aggregating Managed Objects

Aggregation is slightly more complex. It is typically used when the outer object wants to expose another objects implementation of an interface without modification. All managed objects automatically support COM style aggregation with the managed object being used as the inner object (or aggregatee). In order to aggregate a managed object, the unmanaged outer object creates the managed inner object by calling CoCreateInstance passing the outer object’s IUnknown as a OuterUnknown parameter. When an outer IUnknown is passed to a managed object during construction, the managed object caches the interface and uses it as follows:

These three behaviors make it possible to aggregate any managed object. With this type of aggregation relationship, it’s possible to have a single COM object that is partly implemented in managed code (the inner portion) and partly in unmanaged code (the outer portion).

Exposing Inheritance to COM

When managed interfaces are exposed to COM, they always extend IUnknown or IDispatch (See the class System.InteropServices.InterfaceTypeAttribute) even when the interface is inherited from another interface on the managed side. The same applies for the class interface that’s generated for managed classes. For example:

In Managed C++:

Interface IBase {
    void  m1();
}

Interface IDerived : implements IBase {
    void  m2();{
}

class CDerivedImpl : implements IDerived {
    void  m1();   
    void  m2();   
}

In an Exported Type Library:

Interface IBase : public IDispatch {
    void  m1();
}

Interface IDerived : public IDispatch {
    void  m2();
}

Interface _CDerivedImpl : public IDispatch {
    Boolean Equals();
    int GetHashCode()
    // … plus all other methods of object

    void  m1();
    void  m2();
}

coclass CDerivedImpl {
    interface IBase;   
    interface IDerived;   
    interface _CDerivedImpl;   
    interface _Object;   
}

Notice how IBase and IDerived both extend IUnknown in the type library. The inheritance relationship between the two interfaces is intentionally removed. This prevents a COM client from calling methods of the IBase interface through the IDerived interface. In order for a COM client to use either interface, it must explicitly query the coclass for the proper interface before making the call.

Also notice that the class interface contains all the methods on the derived class as well as all the methods on its base class. (System.Object in this case). This is different from the way other (non-class) interfaces are exposed. When late binding through either of these interfaces, only the methods visible on the interface are callable. For example, you could not late bind to method m1 through the IDerived interface. One exception to this rule is with the _Object. The through _Object interface, any method supported by any class in the objects hierarchy can be called. For example, you could safely call m2 late bound through _Object.

An alternative approach would have been to preserve the interface inheritance within the type library. In other words, have the IDerived interface actually extend the IBase interface rather than IUnknown. But, by organizing the interfaces as shown, the IDerived interface becomes more version resilient. Consider what happens to existing users of the IDerived interface when a new method m3 is added to the IBase Interface in the example above.

If the interface inheritance were preserved, the user of the IDerived interface would assume the m2 method was in the 5th slot of the IDerived vtable (after the 3 methods of IUnknown and method m1 from IBase). But, when the new method is added to IBase, the m2 method would actually be relocated to the 6th slot making room for m3 in IBase. When the client tried to make a call on the 5th slot, the call would either go to the implementation of m3 or cause an access violation. Notice that this would happen even if the author of the IBase interface created a new IID for IBase when the new method was added. The reason for the failure is because the definition of the IBase interface is embedded within the definition of IDerived and yet IBase can be changed without the user of IDerived ever know it.

Notice that the client never queried for IBase. The only interface that that client queried for was IDerived, which may have been written by a completely different developer. The only way to prevent this problem would be to change the IID of IBase and every interface that extends IBase.

Now lets consider what happens in the same scenario if the interface inheritance is not exposed to COM. The client that uses IDerived can only calls methods on the IDerived interface. The client assumes that method m1 is in slot 4 of the IBase vtable and the method m2 is in slot 4 of the IDerived v-table. In order to call methods of IBase it must explicitly query for the IID of IBase. When a new method is added to IBase a new IID is generated for the interface automatically (add reference to section that describes automatic IID generation when its added). Now when clients call method m2, the call succeeds because slot of the IDerived v-table still points to method m2 even though a new method was added to IBase. Also notice that if the client queries for the IID of the original IBase interface, the query may succeed or fail depending on whether an implementation of that version of the interface is provided by the class. The author of the class chooses whether to support only the new version of the interface or continue to support the original version. Either way, the client can respond without failing.

Of course managed clients are unaffected by the change in the IBase interface because binding within the runtime is done dynamically.