Tuesday, November 26, 2013

Tridion Workflow - Developing using Dependency Injections

During a workflow design session I noticed that sometimes using a classes design just based on inheritance is not enough and not as flexible as I expected. You can refer to a previous post to get some background Developing Tridion 2013 Workflows.
In that previous post I was reusing code by having an abstract class called BaseActivity and then creating more specialized classes like PublishActivity and RejectActivity that inherit from BaseActivity. This design allows me to reuse and to create specialized functionality for my activities.
There is a concept that I remember from my days studying computer engineering at the University (old and fun days) “Favor composition over inheritance” – Thanks Alvin Reyes for refreshing my mind. Why composition and why not inheritance, well Inheritance is not bad but if we can combine it with composition then we get an stronger result.
I decided to integrate an IoC Container to my workflow implementation so that I can instantiate my objects members (composition) at runtime, giving flexibility and scalability to my implementation. I used Ninject as my IoC container because it is light and easy to use.
Integrating Ninject with a Tridion Workflow Implementation
The following classes design clearly shows two classes hierarchies that are not linked and can be developed / extended independently.

In order to bind those two classes hierarchies I will use Ninject to inject dependencies at runtime based on the class composition.
Binding Activity Classes with Worker Classes
1.      Declare loosely coupled links in the activity classes.
namespace Tridion.ContentManager.Spark.Workflow.Publish {
    public class PublishToDraftActivity : BaseActivity {
       
protected Publisher publisher;
protected Notifier notifier;
}
}
Notice that I am declaring abstract types in order to avoid linking the activity class with a concrete implementation.
2.      Declare a constructor that will receive concrete instances of abstract members.
namespace Tridion.ContentManager.Spark.Workflow.Publish {
    public class PublishToDraftActivity : BaseActivity {
        protected Publisher publisher;
        protected Notifier notifier;

        public PublishToDraftActivity(Publisher publisher, Notifier notifier) {
            this.publisher = publisher;
            this.notifier = notifier;
        }
    }
}
  
            3.      Define injection rules, in this implementation I am using a Ninject module, however you can use an XML file or Ninject naming conventions.

namespace Tridion.ContentManager.Spark.Workflow.Modules {
    public class InjectionModule : NinjectModule {
        public override void Load() {
            Bind<Publisher>().To<StandardPublisher>();
            Bind<Notifier>().To<EmailNotifier>();
        }
    }
}

The injection rules above will bind any Publisher reference to StandardPublisher and any Notifier reference to EmailNotifier. Here the beauty of Dependencies Injections, we can change bindings and it doesn’t affect the activities implementation. For instance we can decide to notify using Rss instead of sending an email, then we should change the binding to use RssNotifier instead of EmailNotifier.


Creating Activity Classes Instances – Linking all together at runtime.

In order to accomplish this I have created a new Workflow Script Executor that will use the Ninject Kernel to create instances instead of System.Reflection. You can get information about how to create your own Workflow Script Executor in this post Extending Workflow Scripts

The Ninject Kernel will instantiate Activity Instances and “Apply” the injection rules when the object is being constructed (the magic happens in the constructor).

External Activity Script
AssemblyTbbId = "tcm:2-49-2048"
Type = "Tridion.ContentManager.Spark.Workflow.Publish.PublishToDraftActivity"

private IExternalActivity GetExternalActivity(string script, bool isExpiration, string currentActivityInstanceId) {
    IEnumerable<Match> matches = _nameValuePairRegex.Matches(script).Cast<Match>();
    Match assemblyMatch = matches.FirstOrDefault(m => m.Groups["Name"].Value.ToUpperInvariant().Trim() == _assemblyTbbIdParam);
    Match typeMatch = matches.FirstOrDefault(m => m.Groups["Name"].Value.ToUpperInvariant().Trim() == _typeParam);

    TcmUri uri = new TcmUri(assemblyMatch.Groups["Value"].Value);
    string typeName = typeMatch.Groups["Value"].Value;

    StreamDownloadClient downloadChannel = new StreamDownloadClient(TcmConstants.LatestStreamDownloadNetTcpEndPointName);
           

    FullVersionInfo fullVersionInfo = (FullVersionInfo)buildingBlockData.VersionInfo;           
    byte[] bytes = ReadContentBytes(downloadChannel.DownloadBinaryContent(uri));
    Assembly assembly =
TemplateAssemblyCache.GetCachedAssembly(bytes, uri, new TcmUri(buildingBlockData.BluePrintInfo.OwningRepository.IdRef), fullVersionInfo.Version.Value, fullVersionInfo.Revision.Value);

    Type type = assembly.GetType(typeName);
    IKernel kernel = new StandardKernel();
    kernel.Load(assembly);

    return (IExternalActivity)kernel.Get(type, contextDataParameter);
}

The Kernel will execute the Injections Modules and will glue everything while the object is being constructed.

No comments:

Post a Comment