The Parts Portal's Dependency Resolver and the Factory Pipeline

One of the features of the Parts Portal is that it supports the instantiation of parts which have service dependencies despite the fact that the Portal doesn't know anything about the services that will be required at runtime. Normally we could simply declare a service in XAML:

<AcropolisApplication.Services>

  <my:AcropolisService Name="AcropolisService1" xmlns:my="clr-namespace:AcropolisApp1;assembly=AcropolisApp1" />

</AcropolisApplication.Services>

Since this is not possible as the parts to instantiate are unknown at compile time we need a way to examine service dependencies at runtime and register the required services dynamically. But before I go into detail let's have a look at how parts are instantiated first. When you create an Acropolis application (or a part with child parts) you will probably simply declare its child parts in XAML:

<AcropolisApplication.ChildParts>

  <my:AcropolisPart Name="AcropolisPart1" xmlns:my="clr-namespace:AcropolisApp1;assembly=AcropolisApp1" />

</AcropolisApplication.ChildParts>

That is all the source code you need to write. The interesting question though is what happens at runtime when the instance is created. You might guess that the Activator class is involved and that is indeed correct. But there is much more to it than that – namely one of the most interesting classes in the Acropolis component model: The FactoryPipeline class. Its purpose is to create and configure instances at runtime and its public interface looks like this:

[ComVisible(false)]

public class FactoryPipeline : ExecutionPipeline<InstanceMoniker>

{

    public static AttachedProperty ConstructorParametersProperty { get; }

    public static AttachedProperty TargetScopeProperty { get; }

    public static FactoryPipeline Instance { get; }

    public InstanceCreationStage InstanceCreationStage { get; }

    public MonikerLinkageStage MonikerLinkageStage { get; }

    public InstanceManipulationStage InstanceSitingStage { get; }

    public InstanceManipulationStage DependencyInjectionStage { get; }

    public InstanceManipulationStage FinalizationStage { get; }

    public static object CreateInstance(InstanceMoniker instanceMoniker);

    public static object CreateInstance(InstanceMoniker moniker, params object[] constructorParameters);

    public static object CreateInstance(InstanceTemplateMoniker moniker, params object[] constructorParameters);

    public static object CreateInstance(Type type, params object[] constructorParameters);

    public static object CreateInstance(TypeMoniker moniker, params object[] constructorParameters);

    public static object EnsureObjectIsCreated(object obj);

}

The creation of an instance is performed in five discrete stages. These are (in the order of execution):

  1. Instance creation
  2. Moniker linkage
  3. Instance siting
  4. Dependency injection
  5. Finalization

As you can see there is a public property for each of those stages on the FactoryPipeline class. This makes it possible to easily customize any stage – except for the moniker linkage stage – by adding a new handler. A handler is a class derived from InstanceCreationHandler and InstanceManipulationHandler respectively which overrides OnExecute(InstanceMoniker). This finally leads us to the DependencyResolver class which is part of the Parts Portal.

public sealed class DependencyResolver : InstanceManipulationHandler

{

    [SecurityPermission(SecurityAction.LinkDemand, ControlAppDomain = true)]

    public DependencyResolver(DirectoryInfo directory, PartCatalogManager manager) : base(true);

    protected override bool OnExecute(InstanceMoniker payload);

}

 

Because it is an instance manipulation handler we can add it to an instance manipulation stage of the pipeline and that way let the pipeline execute our custom code during the configuration of part instances. Since the dependency resolver is responsible for dynamically registering services it adds itself to the dependency injection stage during its own construction.

FactoryPipeline.Instance.DependencyInjectionStage.AddHandler(this, typeof(Part), LinearInsertMode.First);

This ensures that the resolver will actually be called before the part is accessed outside of the Acropolis runtime and before any other handler of the dependency injection stage is executed (at least as long as there isn't another handler which has been added with the linear insert mode set to First). However, note that the dependency injection stage runs after the instance creation stage during which the constructor of the part being instantiated gets executed. This means that the approach described here will fail as soon as a part tries to access a service in its constructor which has not been registered previously. Therefore it is a best practice not to access services in a part's constructor at all. In order to avoid this issue all code that accesses services and usually would be considered part of the constructor should be moved to an override of the method Part.OnFactoryCreationComplete.

The actual implementation of DependencyResolver.OnExecute(InstanceMoniker) is much more obvious than how the method is called by the runtime. It starts off by checking if the payload currently processed by the pipeline is of a type defined in any of the assemblies in the PartPortalCatalog folder and returns immediately if this is not the case. After all, the resolver's job is to take care of parts instantiated by the user at runtime – but not of instances which are actually part of the Portal itself or the Acropolis runtime.

if (Path.GetDirectoryName(payload.TypeMoniker.ResolveToType().Assembly.Location) != assemblyDirectory.FullName)

    return true;

After that a check is performed to verify that the type of the payload is known to the catalog provider currently used by the part catalog manager. In addition to the part types provided by the catalog manager the collection returned by GetPartCatalog will also contain the types of parts which are unavailable for instantiation to the user. An exception is raised if the type is not in the collection returned by the provider. Otherwise a collection containing all types defined in the assemblies in the PartPortalCatalog folder is created which will be used to search for services.

availableParts = partCatalog.CatalogProvider.GetPartCatalog();

if (!availableParts.ContainsKey(payload.TypeMoniker))

    throw new InvalidOperationException("The part type '" + payload.TypeMoniker.TypeId + "' is unknown.");

knownTypes = GetKnownTypes();

The rest of the method is one loop which iterates through all service dependencies of the payload and performs the following steps for each dependency:

  1. Exit the current iteration if the required service has already been registered
  2. Throw an exception if the dependency is mandatory and not of an interface type (this is an intentional restriction of the kinds of service dependencies supported by the Portal)
  3. Exit the current iteration if the dependency is not of an interface type
  4. Get all known (i.e. defined in assemblies in the PartPortalCatalog folder) implementations of the interface (note that GetInterfaceImplementations(Collection<Type>, Type) will only return types that implement the interface and are attributed with the ServiceProductionAttribute attribute)
  5. If there is exactly one implementation it is registered as a global service
  6. Throw an exception if the dependency is not optional and there is no or more than one implementation

foreach (ServiceDependencyDescriptor descriptor in availableParts[payload.TypeMoniker].ServiceDependencies)

{

    dependencyType = descriptor.ServiceDependencyType.ResolveToType();

    if (DependencyRegistry.GetService(dependencyType) == null)

    {

        if (!dependencyType.IsInterface && descriptor.IsRequired)

            throw new InvalidOperationException(

                "Mandatory, non-interface service dependencies are not supported ('" +

                descriptor.ServiceDependencyType.TypeId +

                "').");

        if (dependencyType.IsInterface)

        {

            dependencyImplementations = GetInterfaceImplementations(knownTypes, dependencyType);

            if (dependencyImplementations.Count == 1)

                DependencyRegistry.RegisterGlobalService(

                    dependencyType,

                    ComponentPropertySystem.GetTypeMoniker(dependencyImplementations[0]).CreateInstanceMonikerFor());

     else if (descriptor.IsRequired)

                throw new InvalidOperationException(

                    "There is no or more than one implementation of the service dependency '" +

                    descriptor.ServiceDependencyType.TypeId +

                    "'.");

        }

    }

}

 


This posting is provided "AS IS" with no warranties, and confers no rights.

Comments

  • Anonymous
    August 19, 2007
    Great overview -- thanks.  I much prefer this approach over CAB -- seems far more intuitive.