Containment and Aggregation

CodeGuru content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.


Containment

Click here for larger image

For a long time, containment was considered unfashionable, and originally the ATL didn’t support it, although the ATL has always supported aggregation to some degree. The added opportunity with containment – that you can enhance the inner object method behaviour in the outer object re-implementations – is also its added risk.

Aggregation

Aggregation is a specialization of composition. When an outer object aggregates an interface of an inner object, it doesn’t reimplement the interface – it merely passes the inner object’s interface pointer directly to the client. So, it’s less effort for the outer object. On the other hand, the outer object can’t specialize the behaviour of the inner object.

The goal of aggregation is to convince the client that an interface implemented by the inner object is actually implemented by the outer object – the client should be unaware that there is more than one object in use. However, the aggregation must be carefully managed so that the client can’t use the IUnknown methods on the inner object. For instance, since the client has a direct pointer to the inner object’s IVehicle, the client could use this pointer to call QueryInterface and ask for ICar, at which point the inner object would return E_NOINTERFACE because it doesn’t support ICar. This situation must be avoided in setting up aggregation.


Aggregation

Click here for larger image

Assuming you take care to accommodate the inherent schizophrenia in an aggregated object, aggregation is a more elegant solution than containment, although there is one limitation: if MTS is looking after your object, you can’t use the object as part of an aggregate of non-MTS objects.

Containment

The following notes describe a very simple COM containment project, not using the ATL, just the raw COM API. The code in ..\Vehicle is for a simple COM object, which will be used as the inner contained object. The outer COM object is in ..\Car. A simple console client is in ..\Client. The ..\Registry directory contains some shared registry-update code.

First, the significant code from the inner object, Vehicle. From the Vehicle.idl, you will see that the Vehicle server offers just one interface, IVehicle, with one method, Drive:

[uuid(CBB27840-836D-11d1-B990-0080C824B323)
]
interface IVehicle : IUnknown
{
 HRESULT Drive([in] int i, [in, out] int* ip);
};

The CVehicle class implements this interface:

class CVehicle : public IVehicle
{
public:
 virtual HRESULT __stdcall QueryInterface(
const IID& riid, void** ppv);
 virtual ULONG __stdcall AddRef(void);
 virtual ULONG __stdcall Release(void);

 virtual HRESULT __stdcall Drive(int i, int* ip)
  { (*ip) += i; return S_OK; }

 CVehicle();
 ~CVehicle();
private:
 long m_cRef;
};

The IUnknown methods, the class factory and the registry code is entirely as you would expect. So this inner object offers nothing at all out of the ordinary – no special provisions for containment at all.

Now examine the code for the outer object. First the idl. Note the outer object’s idl imports the inner object’s idl, and that the outer object supports both inner object and outer object interfaces. Note the directories search path includes the path for the vehicle.idl.

// Car.idl
import "ocidl.idl";
import "Vehicle.idl";

[uuid(A9032A50-F54C-11d1-BCB6-0080C824B323)
]
interface ICar : IUnknown
{
 HRESULT Reverse([in] int i, [in, out] int* ip);
};

[	uuid(C724E4D0-F54C-11d1-BCB6-0080C824B323),
 version(1.0)
]
library Car
{
 importlib("stdole2.tlb");

 [	uuid(EA969C30-F54C-11d1-BCB6-0080C824B323),
 ]
 coclass Car
 {
 [default] interface IVehicle;
  interface ICar;
 };
};

The CCar class implements both these interfaces, but note the crucial code in the outer object implementation of the inner interface’s Drive method – its just a wrapper to the inner object’s implementation. Note also the arbitrary Init function which will be used internally to initialize the inner object

class CCar : public IVehicle, public ICar
{
public:
 virtual HRESULT __stdcall QueryInterface(
const IID& riid, void** ppv);
 virtual ULONG __stdcall AddRef(void);
 virtual ULONG __stdcall Release(void);

 virtual HRESULT __stdcall Drive(int i, int* ip)
  { return m_pV->Drive(i, ip); }
 virtual HRESULT __stdcall Reverse(int i, int* ip)
  { (*ip) -= i; return S_OK; }
 HRESULT Init();

 CCar();
 ~CCar();
private:
 long m_cRef;
 IVehicle* m_pV;
};

Given that we have a pointer to an inner object, the code in the constructor and destructor should be common sense:

CCar::CCar() : m_cRef(1), m_pV(NULL)
{ InterlockedIncrement(&g_cObjects); }

CCar::~CCar()
{
 InterlockedDecrement(&g_cObjects);
 if (m_pV != NULL)
  m_pV->Release();
}

The IUnknown methods are implemented in the normal way. The class factory is substantially ordinary, but note the extra call in the CreateInstance to the Init method:

HRESULT __stdcall CCarFactory::CreateInstance (
 IUnknown* pUnkOuter, const IID& riid, void** ppv)
{
 if (pUnkOuter != NULL)
  return CLASS_E_NOAGGREGATION;

 CCar* pCar = new CCar;
 if (pCar == NULL)
  return E_OUTOFMEMORY;

 HRESULT hr = pCar->Init();
 if (FAILED(hr))
 {
  pCar->Release();
  return hr;
 }

 hr = pCar->QueryInterface (riid, ppv);
 pCar->Release();
  return hr;
}

This Init function is implemented to create the inner object:

HRESULT CCar::Init()
{
 HRESULT hr = ::CoCreateInstance(CLSID_Vehicle,
                                 NULL,
                                 CLSCTX_INPROC_SERVER,
                                 IID_IVehicle,
                                 (void**)&m_pV);
 if (FAILED(hr))
  return E_FAIL;
 else
  return S_OK;
}

Finally, the client. This is the expected runtime output:

Walk position = 3
Swim position = 0

The client application code follows. The client wants to use both IVehicle which offers Drive, and ICar which offers Reverse. The client assumes that the Car object offers both interfaces. In order for this to work, we will create a new Car object which internally makes use of the existing Vehicle object.

void main()
{
 int i;
 static int j = 0;
 CoInitialize(NULL);
 IVehicle* pv = NULL;
 HRESULT hr = ::CoCreateInstance(CLSID_Car,
                                 NULL,
                                 CLSCTX_INPROC_SERVER,
                                 IID_IVehicle,
                                 (void**)&pv);

 if (SUCCEEDED(hr))
 {
  for (i = 1; i < 3; i++)
   pv->Drive(i, &j);

  cout << "position = " << j << endl;

  ICar* pc = NULL;

  hr = pv->QueryInterface(IID_ICar, (void**)&pc);

  if (SUCCEEDED(hr))
  {
   for (i = 1; i < 3; i++)
    pc->Reverse(i, &j);

   cout << "position = " << j << endl;
   pc->Release();
  }
  pv->Release();
 }

 CoUninitialize();
}

Aggregation

I’ll now describe a very simple raw COM API aggregation project. The code in ..\ \Vehicle is for a simple COM object, which will be used as the inner object in an aggregation. The outer COM object is in ..\ \Car. A simple console client is in ..\Client. The ..\Registry directory contains some shared registry-update code. Examine Fig 3 below, which is a more detailed version of Fig 2.


Aggregation in Detail

Click here for larger image

First, the significant code from the inner object, Vehicle. From the Vehicle.idl, you will see that the Vehicle server offers just one interface, IVehicle:

[uuid(CBB27840-836D-11d1-B990-0080C824B323)
]
interface IVehicle : IUnknown
{
 HRESULT Drive([in] int i, [in, out] int* ip);
};

In the Vehicle.cpp, you will see another interface – this is not in the IDL and doesn’t have a GUID because we won’t be publishing it – it is purely for internal use in the Vehicle server. This is exactly the same as the standard IUnknown. Our inner object will effectively have two sets of the standard IUnknown methods. One will be used to delegate to the outer object’s IUnknown. The other – the Non-Delegating Unknown – will mostly do the normal work of an unaggregated IUnknown.

interface INDUnknown
{
 virtual HRESULT __stdcall NDQueryInterface(
REFIID riid, void **ppv) = 0;
 virtual ULONG __stdcall NDAddRef() = 0;
 virtual ULONG __stdcall NDRelease() = 0;
};

The Vehicle COM object is implemented in the CVehicle class. This derives from IVehicle (which is a COM interface, and therefore derives from IUnknown), and from the unpublished INDUnknown. Hence, the implementation of the IUnknown methods, the IVehicle methods and the INDUnknown methods. All three of the IUnknown methods are implemented to delegate to the corresponding IUnknown methods of the outer object, using the pointer to the outer interface.

class CVehicle : public IVehicle, public INDUnknown
{
public:
 virtual HRESULT __stdcall QueryInterface(const IID& riid, void** ppv)
 { return m_pUnknownOuter->QueryInterface(riid, ppv); }
 virtual ULONG __stdcall AddRef(void)
 { return m_pUnknownOuter->AddRef(); }
 virtual ULONG __stdcall Release(void)
 { return m_pUnknownOuter->Release(); }

 virtual HRESULT __stdcall NDQueryInterface(
 const IID& riid, void** ppv);
 virtual ULONG __stdcall NDAddRef(void);
 virtual ULONG __stdcall NDRelease(void);

 virtual HRESULT __stdcall Drive(int i, int* ip)
 { (*ip) += i; return S_OK; }

 CVehicle(IUnknown* pUnknownOuter);
 ~CVehicle();

private:
 long m_cRef;
 IUnknown* m_pUnknownOuter;
};

The Non-Delegating AddRef and Release are implemented like normal IUnknown AddRef and Release, but the INDUnknown::QueryInterface is different. The important difference is that requests for IUnknown are unequivocally cast to the internal Non-Delegating INDUnknown – we don’t want to return a pointer to our inner normal IUnknown (because that delegates to the outer object).

HRESULT __stdcall CVehicle::NDQueryInterface(
 const IID& riid, void** ppv)
{
 if (riid == IID_IUnknown)
  *ppv = static_cast<INDUnknown*>(this);
 else if (riid == IID_IVehicle)
  *ppv = static_cast<IVehicle*>(this);
 else {
  *ppv = NULL;
  return E_NOINTERFACE;
 }

 reinterpret_cast<IUnknown*>(*ppv)->AddRef();
 return S_OK;
}

Now the outer object. Again, in the IDL, there’s only one interface:

[uuid(A9032A50-F54C-11d1-BCB6-0080C824B323)
]
interface ICar : IUnknown
{
HRESULT Reverse([in] int i, [in, out] int* ip);
};

The outer COM Object is implemented in Car.cpp. Note the pointer to the inner interface:

class CCar : public ICar
{
public:
 virtual HRESULT __stdcall QueryInterface(
 const IID& riid, void** ppv);
 virtual ULONG __stdcall AddRef(void);
 virtual ULONG __stdcall Release(void);

 virtual HRESULT __stdcall Reverse(int i, int* ip)
 { (*ip) -= i; return S_OK; }

 HRESULT Init();

 CCar();
 ~CCar();
private:
 long m_cRef;
 IUnknown* m_pUnknownInner;
};

The outer object’s QueryInterface. When queried for IVehicle, we delegate to the inner object’s interface. However, remember that m_pUnknownInner is a pointer to the inner object’s Non-Delegating INDUnknown (which doesn’t have a QueryInterface method – so how does this work?)

HRESULT __stdcall CCar::QueryInterface(const IID& riid, void** ppv)
{
 if (riid == IID_IUnknown)
  *ppv = static_cast<IUnknown*>(this);
 else if (riid == IID_ICar)
  *ppv = static_cast<ICar*>(this);
 else if (riid == IID_IVehicle)
  return m_pUnknownInner->QueryInterface(riid, ppv);
 else {
  *ppv = NULL;
  return E_NOINTERFACE;
 }

 static_cast<IUnknown*>(*ppv)->AddRef();
 return S_OK;
}

Here’s the client. As you can see, the client creates a Car COM object, and expects the object to implement both the ICar interface and the IVehicle interface, completely unaware of the aggregation:

void main()
{
 int i;
 static int j = 0;
 CoInitialize(NULL);
 IVehicle* pv = NULL;
 HRESULT hr = ::CoCreateInstance(
  CLSID_Car, NULL, CLSCTX_INPROC_SERVER,
  IID_IVehicle, (void**)&pv);

 if (SUCCEEDED(hr))
 {
  for (i = 1; i < 3; i++)
   pv->Drive(i, &j);

  cout << "position = " << j << endl;

  ICar* pc = NULL;
  hr = pv->QueryInterface(IID_ICar, (void**)&pc);
  if (SUCCEEDED(hr))
  {
   for (i = 1; i < 3; i++)
    pc->Reverse(i, &j);

   cout << "position = " << j << endl;
   pc->Release();
  }
  pv->Release();
 }

 CoUninitialize();
}

ATL Aggregation

From Visual C++, version 6, Aggregation is supported by default. When you use ATL to implement aggregation, you use a series of ATL macros to implement the inner and outer object.

To implement the inner object

  • Add the macro DECLARE_AGGREGATABLE to the class definition. If you use the v6 ATL Object Wizard and check the aggregation box, this is already built-in, and in fact if you don’t want aggregation you must include the macro DECLARE_NOT_AGGREGATABLE instead.

To implement the outer object

  • Declare the macro DECLARE_GET_CONTROLLING_UNKNOWN to define the function GetControllingUnknown.. From v6, this is done for you. Without this function, the controlling unknown cannot be passed to the inner object in the call to CoCreateInstance.
  • Override the methods FinalConstruct and FinalRelease.
  • In FinalConstruct you must call CoCreateInstance, passing the CLSID of the inner object you want to create, and by requesting an IUnknown pointer back. Call the function GetControllingUnknown to supply the inner object with the controlling IUnknown pointer.
  • In FinalRelease, release the inner object’s IUnknown.
  • Add the COM_INTERFACE_ENTRY_AGGREGATE macro to the outer object’s COM map, specifying the inner object’s interface and the IUnknown pointer on the inner object.
  • Add the interface definition for the inner object to the .idl file of the outer object, and reference that interface in the coclass.

Note: To ensure that the inner object does not do something while being created to release the outer object, it is usually a good idea to declare the macro DECLARE_PROTECT_FINAL_CONSTRUCT. Again, v6 does this for you. How could this be an issue? Well, consider this: the inner object might support only conditional aggregation. In other words, it will allow itself to be aggregated only if the outer object attempting aggregation supports some particular interface. So, the inner object would QI the outer object and only return success on the CoCreateInstance if it likes the outer object’s interfaces. You can see how easy it would be for the inner object to accidentally release the outer object in such a scenario.

ATL Aggregation Macros

// atlcom.h
#define DECLARE_GET_CONTROLLING_UNKNOWN() public:\
 virtual IUnknown* GetControllingUnknown() { return GetUnknown(); }
#define DECLARE_PROTECT_FINAL_CONSTRUCT()\
 void InternalFinalConstructAddRef() { InternalAddRef(); }\
 void InternalFinalConstructRelease() { InternalRelease(); }

From the folowing you can see that when an object is being aggregated, its created as a CComAggObject, and when its being created independently, its created as a CComObject. If you want to disallow aggregation, use DECLARE_NOT_AGGREGATABLE – for efficiency reasons, you should use this with EXE-packaged classes (since aggregates must be in-process).

#define DECLARE_NOT_AGGREGATABLE(x) public:\
 typedef CComCreator2< CComCreator< CComObject< x > >, \
 CComFailCreator<CLASS_E_NOAGGREGATION> > _CreatorClass;
#define DECLARE_AGGREGATABLE(x) public:\
 typedef CComCreator2< CComCreator< CComObject< x > >, \
 CComCreator< CComAggObject< x > > > _CreatorClass;

If, for some strange reason, you want objects of your class to be created ONLY as part of an aggregation, use DECLARE_ONLY_AGGREGATABLE:

#define DECLARE_ONLY_AGGREGATABLE(x) public:\
 typedef CComCreator2< CComFailCreator<E_FAIL>, \
 CComCreator< CComAggObject< x > > > _CreatorClass;

If you want to have your objects created from the same class whether they are being aggregated or not, use DECLARE_POLY_AGGREGATABLE: this means your COM objects will always be created as objects of type CComPolyObject:

#define DECLARE_POLY_AGGREGATABLE(x) public:\
 typedef CComCreator< CComPolyObject< x > > _CreatorClass;
template <class T1, class T2>
class CComCreator2
{
public:
 static HRESULT WINAPI CreateInstance(void* pv,
                                      REFIID riid,
                                      LPVOID* ppv)
 {
  ATLASSERT(*ppv == NULL);
  return (pv == NULL) ?
  T1::CreateInstance(NULL, riid, ppv) :
  T2::CreateInstance(pv, riid, ppv);
 }
};

These choices can be summarised like this:

Macro

Normal Case

Aggregated Case

DECLARE_NOT_AGGREGATABLE

CComObject

Not supported

DECLARE_AGGREGATABLE

CComObject

CComAggObject

DECLARE_ONLY_AGGREGATABLE

Not supported

CComAggObject

DECLARE_POLY_AGGREGATABLE

CComPolyObject

CComPolyObject

Project 1: ATL Inner Object and Client

In this simple project, we’ll create a “Vehicle” COM object server where the object is suitable for aggregating as an inner object (using the ATL from MSVC v6, the default COM object generated by the ATL Object Wizard is already suitable for aggregation, so we don’t have to do anything special at all). We will also write a client that uses this object. Later, we will aggregate this Vehicle object as the inner object in an aggregation.

  1. Create a new ATL COM AppWizard project, with all defaults. Insert a new ATL Object with the Object Wizard. Give it the short name “Vehicle” and accept IVehicle as the default interface and CVehicle as the class, etc. Leave everything default (ie dual interface, supports aggregation). Add one method to the IVehicle interface, called Drive, with this parameter list:
    [in] int i, [in, out] int* ip
    
  2. In the CVehicle class, implement this function as shown below. Then build the server.

    (*ip) += i;
    return S_OK;
    
  3. Now create another project – for the client. Make it a Win32 Console Application. Above main, #import the Vehicle type library. Code main to test the Vehicle object, build and test.

    try
    {
     IVehiclePtr pv(CLSID_Vehicle);
    
     for (i = 1; i < 3; i++)
      pv->Drive(i, &j);
    
     cout << "position = " << j << endl;
    }
    catch (_com_error& e)
    {
     cout << e.ErrorMessage() << "\n";
    }
    

Project 2: ATL Outer Object Aggregation

In this project, we'll create a "Car" COM object server where the object is suitable as the outer object in an aggregation - for this, we'll need to do some extra work. The outer Car object will aggregate the inner Vehicle object. We will also enhance our client to use the outer object and - transparently - also the aggregated innner object.

  1. Create a new ATL COM AppWizard project, with all defaults. Insert a new ATL Object with the Object Wizard. Give it the short name "Car" and accept ICar as the default interface and CCar as the class, etc. Again, leave everything default (ie dual interface, supports aggregation). Add a method to the ICar interface, called Reverse, with the same parameter list as the IVehicle::Drive, ie:

    [in] int i, [in, out] int* ip
    

  2. In the CCar class, implement this method like this:

    (*ip) -= i;
    return S_OK;
    
  3. Copy and paste the definition of the IVehicle interface from the ATLVehicle IDL file into the ATLCar IDL file - put it above or below the ICar interface. In the library section, list the IVehicle interface as a second supported interface in the Car coclass.
  4. Declare a new member variable in the CCar class: an IUnknown pointer called m_pInner - this will eventually hold the IUnknown pointer on the aggregated inner object. Initialize this to NULL in the constructor. In the CCar class COM map, add an entry for IVehicle - this will be a special aggregation macro that evaluates out to a QueryInterface through the inner object's IUnknown:

    COM_INTERFACE_ENTRY_AGGREGATE(IID_IVehicle, m_pInner)
    
  5. Also in the CCar class declaration, add the DECLARE_GET_CONTROLLING_UNKNOWN() macro - this evaluates out to declaring a function called GetControllingUnknown, which will get the outer object's IUnknown pointer. Also declare these two functions:

    void FinalRelease();
    HRESULT FinalConstruct();
    
  6. In the Car's implementation file, first re-declare the CLSID for the Vehicle object:

    const CLSID CLSID_Vehicle =
    {0x5FD7754E,0xAE66,0x11D3,
    {0x80,0xE9,0x00,0x60,0x08,0x43,0x8F,0x29}};
    

    Note: You can copy this from the ATLVehicle_i.c. Then implement the two new functions as shown below. The FinalConstruct will instantiate the inner object, using CoCreateInstance like any regular client; and the FinalRelease will Release the inner object:

    HRESULT CCar::FinalConstruct()
    {
     return CoCreateInstance(CLSID_Vehicle,
                             GetControllingUnknown(),
                             CLSCTX_ALL, IID_IUnknown,
                             (void**) &m_pInner);
    }
    
    void CCar::FinalRelease()
    {
     if (NULL != m_pInner)
      m_pInner->Release();
    }
    
  7. Now, update the client: first change the #import to import the ATLCar type library and not the ATLVehicle type library (remember the ATLCar type library will also expose the IVehicle interface). Also change the client code to use the outer (and aggregated inner) object - in this way, the client is only directly aware of the outer object, and assumes the outer object implements both IVehicle and ICar. Build and test.

    try
    {
    // IVehiclePtr pv(CLSID_Vehicle);
     IVehiclePtr pv(CLSID_Car);
     for (i = 1; i < 3; i++)
      pv->Drive(i, &j);
    
     cout << "position = " << j << endl;
    
     ICarPtr pc = pv;
     for (i = 1; i < 3; i++)
      pc->Reverse(i, &j);
    
     cout << "position = " << j << endl;
    }
    

Project 3: ATL Outer Object Containment

In this final project, we'll write a version of the "Car" COM object server where the object is suitable as the outer object in a containment solution - the outer Car object will contain the inner Vehicle object. We'll also enhance our client to use the outer object and the contained innner object.

  1. Take a copy of your 3 earlier projects: the ATLVehicle, ATLCar and ATLClient. The inner Vehicle project does not need to change - it is immaterial whether the inner object is being contained or aggregated.
  2. In the ATLCar IDL file, we could use the same technique as with aggregation (ie, redeclare the IVehicle interface). Instead, import the Vehicle's IDL, by adding this line to the library section just before the coclass:

    import "..\ATLVehicle\ATLVehicle.idl";
    
  3. Then list the IVehicle as a supported interface in the coclass as with aggregation.

  4. Make sure the client code still #imports the outer object's type library:

    #import "..\ATLCar.tlb" no_namespace named_guids
    
  5. All the major work is done in the Car code. First, right-click on ATLCar classes, and select Implement Interface, then select the ATLVehicle type library, and select the IVehicle interface, to get this code in the Car header file:

    #import "..\ATLVehicle\ATLVehicle.tlb" raw_interfaces_only, \
    raw_native_types, no_namespace, named_guids
    

    Note: By default, the high-level error-handling methods use the COM support classes _bstr_t and variant_t in place of the BSTR and VARIANT data types and raw COM interface pointers. These classes encapsulate the details of allocating and deallocating memory storage for these data types, and greatly simplify type casting and conversion operations. The raw_native_types attribute is used to disable the use of these COM support classes in the high-level wrapper functions, and force the use of low-level data types instead.

    The raw_interfaces_only attribute suppresses the generation of error-handling wrapper functions and __declspec(property) declarations that use those wrapper functions.

    ...you'll also get this code in the multiple inheritance for the CCar class:

    public IDispatchImpl<IVehicle,
                         &IID_IVehicle,
                         &LIBID_ATLVEHICLELib>
    

    ...and this code in the CCar class body:

    STDMETHOD(Drive)(INT i, INT * ip)
    {
    	return E_NOTIMPL;
    }
    
  6. If the wizard doesn't do it for you, you'll need to update the COM map (and don't just assume the wizard will do it for you, because sometimes it doesn't):

    COM_INTERFACE_ENTRY(IVehicle)
    
  7. In the CCar class, add a new member variable, an IVehicle pointer called m_pVehicle. Initialize this in the constructor to NULL. Now change the implementation of the Drive method to use this pointer to delegate the Drive behaviour (remember, the CCar::Drive is only a stub to the CVehicle::Drive):

    if (ip == NULL)
     return E_POINTER;
    return m_pVehicle->Drive(i, ip);
    
  8. Also declare FinalConstruct and FinalRelease in the CCar class, and implement them as shown below. Note: although these look superficially the same as the aggregation version, there are some differences. Build and test.

    HRESULT CCar::FinalConstruct()
    {
    return CoCreateInstance(CLSID_Vehicle,
                            NULL,
                            CLSCTX_INPROC_SERVER,
                            IID_IVehicle,
                            (void**) &m_pVehicle);
    }
    
    void CCar::FinalRelease()
    {
     m_pVehicle->Release();
    }
    

    Summary

    So, we've peered under the hood to look at the underlying mechanics of containment and aggregation - and did you get the quintessentially COM way the two inner object unknowns are managed? We then examined the way the ATL supports the two strategies. Which strategy you use depends on what you're trying to do. Although containment is still not exactly fashionable, it is easy to do and doesn't fall foul of other constraints like MTS hosting. Aggregation will always be faster, although the internal code is a little trickier. Horses for courses.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read