Extension methods that add fluent elegance

Last night I was updating some integration tests for one of my projects. I have many test methods that configure a directory path from several sources of information and found that path concatenation often results in ugly unreadable code.

Consider the scenario where you have a base path to which you want to add several other directory names.

namespace ConsoleApplication1
{
    using System;
    using System.IO;

    class Program
    {
        static void Main(String[] args)
        {
            String basePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            const String ProductName = "SomeProductName";
            String instanceName = Guid.NewGuid().ToString();
            const String dataName = "DataStore";

            String hardToReadEvaluation = Path.Combine(Path.Combine(Path.Combine(basePath, ProductName), instanceName), dataName);

            Console.WriteLine(hardToReadEvaluation);
            Console.ReadKey();
        }
    }
}

The statement with multiple Path.Combine evaluations is very difficult to read. A simple extension method can turn this into a fluent API design to achieve the same result and allow the intention of the code to be crystal clear.

namespace ConsoleApplication1
{
    using System;
    using System.IO;

    public static class Extensions
    {
        public static String AppendPath(this String basePath, String appendValue)
        {
            return Path.Combine(basePath, appendValue);
        }
    }

    class Program
    {
        static void Main(String[] args)
        {
            String basePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            const String ProductName = "SomeProductName";
            String instanceName = Guid.NewGuid().ToString();
            const String dataName = "DataStore";

            String fluentEvaluation = basePath.AppendPath(ProductName).AppendPath(instanceName).AppendPath(dataName);

            Console.WriteLine(fluentEvaluation);
            Console.ReadKey();
        }
    }
}

The code is now much more readable. You no longer need to backtrack along the line of code to figure out which variables related to which operation in the evaluation as the code now reads fluently from left to right.

Configuration support for custom IErrorHandler in WCF

My post about implementing IErrorHandler for WCF a few years ago is my second top post on this site. My Toolkit project on Codeplex has had support for hooking up an IErrorHandler using an attribute on the service implementation class which is an extension of the original post.

My preference has always been to hook up IErrorHandler using an attribute to avoid any potential security holes. This would be a scenario where the configuration for IErrorHandler is removed and exception shielding is no longer available to prevent potentially sensitive information from being displayed to clients. I am now playing with workflow services and am not able to use an attribute for this purpose. I no longer have a choice and must use a configuration based IErrorHandler implementation.

I have added configuration support for IErrorHandler to my Toolkit project based on the original posts above to assist with this process.

namespace Neovolve.Toolkit.Communication
{
    using System;
    using System.Configuration;
    using System.ServiceModel.Configuration;

    public class ErrorHandlerElement : BehaviorExtensionElement
    {
        public const String ErrorHandlerTypeAttributeName = "type";

        private ConfigurationPropertyCollection _properties;

        protected override Object CreateBehavior()
        {
            return new ErrorHandlerAttribute(ErrorHandlerType);
        }

        public override Type BehaviorType
        {
            get
            {
                return typeof(ErrorHandlerAttribute);
            }
        }

        [ConfigurationProperty(ErrorHandlerTypeAttributeName)]
        public String ErrorHandlerType
        {
            get
            {
                return (String)base[ErrorHandlerTypeAttributeName];
            }

            set
            {
                base[ErrorHandlerTypeAttributeName] = value;
            }
        }

        protected override ConfigurationPropertyCollection Properties
        {
            get
            {
                if (_properties == null)
                {
                    ConfigurationPropertyCollection properties = new ConfigurationPropertyCollection();

                    properties.Add(
                        new ConfigurationProperty(
                            ErrorHandlerTypeAttributeName, typeof(String), String.Empty, null, null, ConfigurationPropertyOptions.IsRequired));

                    _properties = properties;
                }

                return _properties;
            }
        }
    }
}

The ErrorHandlerElement class allows for WCF configuration to configure an IErrorHandler for a service. This class provides the configuration support to define the type of error handler to use. The CreateBehavior method simply forwards the configured IErrorHandler type to the ErrorHandlerAttribute class that is already in the toolkit.

<?xml version="1.0" ?>
<configuration>
    <system.serviceModel>
        <behaviors>
            <serviceBehaviors>
                <behavior name="ErrorHandlerBehavior">
                    <errorHandler type="Neovolve.Toolkit.IntegrationTests.Communication.KnownErrorHandler, Neovolve.Toolkit.IntegrationTests"/>
                </behavior>
            </serviceBehaviors>
        </behaviors>
        <extensions>
            <behaviorExtensions>
                <add name="errorHandler"
                     type="Neovolve.Toolkit.Communication.ErrorHandlerElement, Neovolve.Toolkit"/>
            </behaviorExtensions>
        </extensions>
        <services>
            <service behaviorConfiguration="ErrorHandlerBehavior"
                     name="Neovolve.Toolkit.IntegrationTests.Communication.TestService">
                <endpoint address=""
                          binding="basicHttpBinding"
                          bindingConfiguration=""
                          contract="Neovolve.Toolkit.IntegrationTests.Communication.ITestService"/>
            </service>
        </services>
    </system.serviceModel>
</configuration> 

The WCF configuration defines the ErrorHandlerElement class as a behavior extension. This service behavior can then use this extension and identify the type of IErrorHandler to hook into the service.

This class has been added into the next beta of my Toolkit. You can get it from here.

Custom IssuerNameRegistry to reduce WIF team development pain

I have been implementing WIF into my hosted synchronization project over recent months. One of the issues that I keep hitting with WIF is managing STS certificates between multiple machines. In my case I have a desktop and a laptop that I use for development. The same issue outlined here applies to working in a development team.

The WIF SDK makes it easy to get up and running with an STS. The wizard application creates an STS project and development certificates that are then integrated into your [VS] solution. The certificates are created on the local machine and are specific to that machine. One is the signing certificate with the default name of STSTestCert and the other is the encrypting certificate with the default name of DefaultApplicationCertificate.

The WIF configuration usually refers to these certificates using the subject distinguished name of the certificate. This will work where multiple development machines use certificates with the same subject where those certificates where created on each machine. Unfortunately the configuration for the Relying Party application identifies the trusted issuer certificate using a thumbprint. This thumbprint will be different across each machine.

<service name="Neovolve.Jabiru.Server.Service.ExchangeSession">
    <audienceUris>
        <add value="https://localhost/Jabiru/DataExchange.svc" />
    </audienceUris>
    <issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
        <trustedIssuers>
            <!-- This is the thumbprint for the certificate used sign the certificate - STSTestCert -->
            <add thumbprint="3E8D41EA2AF035D352D07FE46AACE352AD2F32B0"
                 name="https://localhost/JabiruSts/Service.svc" />
        </trustedIssuers>
    </issuerNameRegistry>
</service>

The above configuration is for one of the services in my project. The configuration identifies the trusted issuer using a specific certificate thumbprint.

One solution to this issue is to copy the certificates around each development machine. I’m not a fan of this solution as trying to use local certificates on other machines is problematic. My preference is to have each development machine generate their own certificates using the same subject names. If required this functionality could be easily wrapped up in a batch file that is part of the solution. This method of working with local certificates is the same as a development team getting IIS on each workstation to create self-signed certificates with the same name (localhost for example).

There is an alternative solution however as WIF provides an extensibility point that allows for a different implementation. The type attribute of the issuerNameRegistry node in the above configuration above identifies the type that provides the IssuerNameRegistry class for the service. There is only one implementation provided with WIF which is ConfigurationBasedIssuerNameRegistry. This class is hard-coded to only deal with certificate thumbprints. Creating a type that can handle more certificate matching options using X509FindType will be the answer to this restriction.

namespace Neovolve.Jabiru.Server.Security
{
    using System;
    using System.Security.Cryptography.X509Certificates;

    public struct IssuerCertificateMapping
    {
        public X509FindType FindType;

        public String FindValue;

        public String IssuerName;
    }
}

The IssuerCertificateMapping struct will define the relationship between an issuer name and the certificate matching criteria. The ConfiguredCertificateIssuerNameRegistry class will read the issuer configuration and provide the logic for matching against the security token certificate when the GetIssuerName is invoked.

namespace Neovolve.Jabiru.Server.Security
{
    using System;
    using System.Collections.Generic;
    using System.Configuration;
    using System.Globalization;
    using System.IdentityModel.Tokens;
    using System.Security.Cryptography.X509Certificates;
    using System.Xml;
    using Microsoft.IdentityModel.Tokens;
    using Neovolve.Jabiru.Server.Security.Properties;

    public class ConfiguredCertificateIssuerNameRegistry : IssuerNameRegistry
    {
        private readonly List<IssuerCertificateMapping> _trustedIssuers = new List<IssuerCertificateMapping>();

        public ConfiguredCertificateIssuerNameRegistry()
        {
        }

        public ConfiguredCertificateIssuerNameRegistry(XmlNodeList customConfiguration)
        {
            if (customConfiguration == null)
            {
                throw new ArgumentNullException("customConfiguration");
            }

            for (Int32 index = 0; index < customConfiguration.Count; index++)
            {
                XmlNode node = customConfiguration[index];

                if (node.Name != "trustedIssuers")
                {
                    continue;
                }

                LoadIssueConfiguration(node);
            }
        }

        public override String GetIssuerName(SecurityToken securityToken)
        {
            if (securityToken == null)
            {
                throw new ArgumentNullException("securityToken");
            }

            X509SecurityToken token = securityToken as X509SecurityToken;

            if (token == null)
            {
                return null;
            }

            X509Certificate2 tokenCertificate = token.Certificate;

            if (tokenCertificate == null)
            {
                return null;
            }

            for (Int32 index = 0; index < TrustedIssuers.Count; index++)
            {
                IssuerCertificateMapping mapping = TrustedIssuers[index];

                if (CertificateMatchesConfigurationItem(tokenCertificate, mapping))
                {
                    return mapping.IssuerName;
                }
            }

            return null;
        }

        private static Boolean CertificateMatchesConfigurationItem(X509Certificate2 tokenCertificate, IssuerCertificateMapping mapping)
        {
            String findValue = mapping.FindValue;

            switch (mapping.FindType)
            {
                case X509FindType.FindByThumbprint:

                    return tokenCertificate.Thumbprint == findValue;

                case X509FindType.FindBySubjectName:

                    return StripToCommonName(tokenCertificate.SubjectName) == findValue;

                case X509FindType.FindBySubjectDistinguishedName:

                    return tokenCertificate.Subject == findValue;

                case X509FindType.FindByIssuerName:

                    return StripToCommonName(tokenCertificate.IssuerName) == findValue;

                case X509FindType.FindByIssuerDistinguishedName:

                    return tokenCertificate.IssuerName.Name == findValue;

                case X509FindType.FindBySerialNumber:

                    return tokenCertificate.SerialNumber == findValue;

                    // case X509FindType.FindByTimeValid:
                    // break;
                    // case X509FindType.FindByTimeNotYetValid:
                    // break;
                    // case X509FindType.FindByTimeExpired:
                    // break;
                    // case X509FindType.FindByTemplateName:
                    // break;
                    // case X509FindType.FindByApplicationPolicy:
                    // break;
                    // case X509FindType.FindByCertificatePolicy:
                    // break;
                    // case X509FindType.FindByExtension:
                    // break;
                    // case X509FindType.FindByKeyUsage:
                    // break;
                    // case X509FindType.FindBySubjectKeyIdentifier:
                    // break;
                default:
                    throw new NotSupportedException();
            }
        }

        private static Nullable<IssuerCertificateMapping> ConvertNodeToCertificateConfiguration(XmlNode node)
        {
            if (node == null)
            {
                throw new ArgumentNullException("node");
            }

            if (node.Attributes == null)
            {
                return null;
            }

            XmlNode findTypeNode = node.Attributes.GetNamedItem("findType");

            if (findTypeNode == null)
            {
                throw new ConfigurationErrorsException(Resources.ConfiguredCertificateIssuerNameRegistry_FindTypeNotConfigured);
            }

            String configuredFindType = findTypeNode.Value;
            X509FindType findType;

            if (Enum.TryParse(configuredFindType, out findType) == false)
            {
                String message = String.Format(
                    CultureInfo.CurrentCulture, Resources.ConfiguredCertificateIssuerNameRegistry_InvalidFindTypeConfigured, configuredFindType);

                throw new ConfigurationErrorsException(message);
            }

            XmlNode findValueNode = node.Attributes.GetNamedItem("findValue");

            if (findValueNode == null)
            {
                throw new ConfigurationErrorsException(Resources.ConfiguredCertificateIssuerNameRegistry_FindValueNotConfigured);
            }

            String findValue = findValueNode.Value;

            if (String.IsNullOrWhiteSpace(findValue))
            {
                throw new ConfigurationErrorsException(Resources.ConfiguredCertificateIssuerNameRegistry_FindValueConfigurationIsEmpty);
            }

            XmlNode nameNode = node.Attributes.GetNamedItem("name");

            if (nameNode == null)
            {
                throw new ConfigurationErrorsException(Resources.ConfiguredCertificateIssuerNameRegistry_NameNotConfigured);
            }

            String name = nameNode.Value;

            if (String.IsNullOrWhiteSpace(name))
            {
                throw new ConfigurationErrorsException(Resources.ConfiguredCertificateIssuerNameRegistry_NameConfigurationIsEmpty);
            }

            return new IssuerCertificateMapping
                   {
                       FindType = findType, 
                       FindValue = findValue, 
                       IssuerName = name
                   };
        }

        private static String StripToCommonName(X500DistinguishedName distinguishedName)
        {
            String name = distinguishedName.Name;

            if (String.IsNullOrWhiteSpace(name))
            {
                return null;
            }

            Int32 commonNameIndex = name.LastIndexOf("CN=");

            if (commonNameIndex < 0)
            {
                return null;
            }

            commonNameIndex += 3;

            if (commonNameIndex > name.Length)
            {
                return null;
            }

            return name.Substring(commonNameIndex);
        }

        private void LoadIssueConfiguration(XmlNode node)
        {
            if (node == null)
            {
                throw new ArgumentNullException("node");
            }

            foreach (XmlNode childNode in node.ChildNodes)
            {
                if (childNode.Name == "clear")
                {
                    TrustedIssuers.Clear();
                }
                else
                {
                    Nullable<IssuerCertificateMapping> issuerCertificateConfiguration = ConvertNodeToCertificateConfiguration(childNode);

                    if (issuerCertificateConfiguration == null)
                    {
                        continue;
                    }

                    if (childNode.Name == "add")
                    {
                        TrustedIssuers.Add(issuerCertificateConfiguration.Value);
                    }
                    else if (childNode.Name == "remove")
                    {
                        TrustedIssuers.Remove(issuerCertificateConfiguration.Value);
                    }
                }
            }
        }

        public List<IssuerCertificateMapping> TrustedIssuers
        {
            get
            {
                return _trustedIssuers;
            }
        }
    }
}

The ConfiguredCertificateIssuerNameRegistry parses the RP configuration to search for add, remove and clear elements. The add and remove elements then need to define the findType (X509FindType), findValue and name attributes. The set of trusted issues becomes the reference point for mapping security token certificates to issuer names.

This class allows for a more flexible mapping between issuer names and certificates. The example configuration above can then be modified to use the subject name instead of thumbprint.

<service name="Neovolve.Jabiru.Server.Service.ExchangeSession">
    <audienceUris>
        <add value="https://localhost/Jabiru/DataExchange.svc" />
    </audienceUris>
    <issuerNameRegistry type="Neovolve.Jabiru.Server.Security.ConfiguredCertificateIssuerNameRegistry, Neovolve.Jabiru.Server.Security">
        <trustedIssuers>
            <add findType="FindBySubjectDistinguishedName"
                findValue="CN=STSTestCert"
                name="https://localhost/JabiruSts/Service.svc" />
        </trustedIssuers>
    </issuerNameRegistry>
</service>

The RP can then correctly identify issuer certificates created with the same subject on different machines.

Custom Workflow activity for business failure evaluation–Wrap up

My latest series on custom WF activities has provided a solution for managing business failures.

The BusinessFailureEvaluator<T> activity evaluates a single business failure and may result in an exception being thrown.

The BusinessFailureScope<T> activity manages multiple business failures and may result in an exception being thrown for a set of failures.

The following is a list of all the posts in this series.

This workflow activity and several others can be found in my Neovolve.Toolkit project (in 1.1 Beta with latest source code here) on CodePlex.

Custom Workflow activity for business failure evaluation–Part 6

The previous post in this series provided the custom activity that manages multiple business failures in WF. Providing adequate designer support was one of the design goals of this series. This post will outline the designer support for the BusinessFailureEvaluator<T> and BusinessFailureScope<T> activities.

BusinessFailureEvaluator<T>

The BusinessFailureEvaluator<T> evaluates a single business failure. There is no support for child activities which makes the designer very simple.

<sap:ActivityDesigner x:Class="Neovolve.Toolkit.Workflow.Design.Presentation.BusinessFailureEvaluatorDesigner"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sap="clr-namespace:System.Activities.Presentation;assembly=System.Activities.Presentation"
    xmlns:sapv="clr-namespace:System.Activities.Presentation.View;assembly=System.Activities.Presentation">
  <sap:ActivityDesigner.Icon>
    <DrawingBrush>
      <DrawingBrush.Drawing>
        <ImageDrawing>
          <ImageDrawing.Rect>
            <Rect Location="0,0" Size="16,16" ></Rect>
          </ImageDrawing.Rect>
          <ImageDrawing.ImageSource>
            <BitmapImage UriSource="shield.png" ></BitmapImage>
          </ImageDrawing.ImageSource>
        </ImageDrawing>
      </DrawingBrush.Drawing>
    </DrawingBrush>
  </sap:ActivityDesigner.Icon>
</sap:ActivityDesigner>
The XAML for the designer simply identifies the image to use for the activity on the design surface.

The activity has a generic type argument that defaults to Int32 when the activity is dropped onto the designer. This type is not always suitable for the purposes of the application so the generic type argument needs to be updatable.

namespace Neovolve.Toolkit.Workflow.Design.Presentation
{
    using System;
    using System.Diagnostics;
    using Neovolve.Toolkit.Workflow.Activities;

    public partial class BusinessFailureEvaluatorDesigner
    {
        [DebuggerNonUserCode]
        public BusinessFailureEvaluatorDesigner()
        {
            InitializeComponent();
        }

        protected override void OnModelItemChanged(Object newItem)
        {
            base.OnModelItemChanged(newItem);

            GenericArgumentTypeUpdater.Attach(ModelItem);
        }
    }
}

The code behind the designer supports this by attaching an ArgumentType property to the ModelItem when it is assigned to the designer.

This attached property allows the generic type of the activity to be changed to another type.

BusinessFailureScope<T>

The BusinessFailureScope<T> allows for multiple business failures to be stored against the scope so that they can be thrown together rather than one at a time.

<sap:ActivityDesigner x:Class="Neovolve.Toolkit.Workflow.Design.Presentation.BusinessFailureScopeDesigner"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sap="clr-namespace:System.Activities.Presentation;assembly=System.Activities.Presentation" 
    xmlns:sacdt="clr-namespace:System.Activities.Core.Presentation.Themes;assembly=System.Activities.Core.Presentation"
    xmlns:sapv="clr-namespace:System.Activities.Presentation.View;assembly=System.Activities.Presentation">
  <sap:ActivityDesigner.Icon>
    <DrawingBrush>
      <DrawingBrush.Drawing>
        <ImageDrawing>
          <ImageDrawing.Rect>
            <Rect Location="0,0" Size="16,16" ></Rect>
          </ImageDrawing.Rect>
          <ImageDrawing.ImageSource>
            <BitmapImage UriSource="shield_go.png" ></BitmapImage>
          </ImageDrawing.ImageSource>
        </ImageDrawing>
      </DrawingBrush.Drawing>
    </DrawingBrush>
  </sap:ActivityDesigner.Icon>
  <ContentPresenter x:Uid="ContentPresenter_1" Style="{x:Static sacdt:DesignerStylesDictionary.SequenceStyle}" Content="{Binding}" />
</sap:ActivityDesigner>

The XAML for the designer does two things. Firstly it identifies the icon the activity uses on the designer. Secondly, it identifies that the style of the content presenter is the same one that the SequenceDesigner uses for the Sequence activity. This style provides the support for displaying arrows, drag/drop behaviour and animation on the designer for working with child activities. This style is available from the DesignerStylesDictionary.SequenceStyle property. The System.Activities.Core.Presentation assembly exposes this type and is a reference of the designer project.

Like the BusinessFailureEvalator<T> activity, the BusinessFailureScope has a generic type argument that defaults to Int32 when the activity is dropped onto the designer. The code behind the designer makes this type updatable in the same way.

namespace Neovolve.Toolkit.Workflow.Design.Presentation
{
    using System;
    using System.Diagnostics;
    using Neovolve.Toolkit.Workflow.Activities;

    public partial class BusinessFailureScopeDesigner
    {
        [DebuggerNonUserCode]
        public BusinessFailureScopeDesigner()
        {
            InitializeComponent();
        }

        protected override void OnModelItemChanged(Object newItem)
        {
            base.OnModelItemChanged(newItem);

            GenericArgumentTypeUpdater.Attach(ModelItem);
        }
    }
}

The property attached by GenericArgumentTypeUpdater allows generic type to be changed using the ArgumenType property.

This post has demonstrated the designer support for the BusinessFailureEvaluator<T> and BusinessFailureScope<T> activities. Workflows can now use these two activities to manage business failures.

Custom Workflow activity for business failure evaluation–Part 5

The previous post in this series provided the custom activity that evaluates a single business failure in WF. One of the design goals of this series is to support the evaluation and notification of multiple failures. This post will provide a custom activity that supports this design goal.

At the very least, this activity needs to be able to contain multiple BusinessFailureEvaluator<T> activities. The design of this control will be like the Sequence activity where it can define and execute a collection of child activities.

There is no reason to restrict the child activities to the BusinessFailureEvaluator activity type so it will allow any child activity type. The screenshot above demonstrates this by adding an ExecuteBookmark activity in the middle of the business evaluators within the scope.

image

The activity has the same behaviour as BusinessFailureEvaluator for managing the generic type on the activity. It defaults to using Int32 and exposes an ArgumentType property to change the type after the activity is on the designer.

namespace Neovolve.Toolkit.Workflow.Activities
{ 
    using System;
    using System.Activities;
    using System.Activities.Presentation;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Drawing;
    using System.Windows.Markup;
    using Neovolve.Toolkit.Workflow.Extensions;

    [ContentProperty("Activities")]
    [ToolboxBitmap(typeof(ExecuteBookmark), "shield_go.png")]
    [DefaultTypeArgument(typeof(Int32))]
    public sealed class BusinessFailureScope<T> : NativeActivity where T : struct
    {
        private readonly Variable<Int32> _activityExecutionIndex = new Variable<Int32>();

        public BusinessFailureScope()
        {
            Activities = new Collection<Activity>();
            Variables = new Collection<Variable>();
        }

        protected override void CacheMetadata(NativeActivityMetadata metadata)
        {
            metadata.RequireExtension<BusinessFailureExtension<T>>();
            metadata.AddDefaultExtensionProvider(() => new BusinessFailureExtension<T>());
            metadata.SetChildrenCollection(Activities);
            metadata.SetVariablesCollection(Variables);
            metadata.AddImplementationVariable(_activityExecutionIndex);
        }

        protected override void Execute(NativeActivityContext context)
        {
            if (Activities == null)
            {
                return;
            }

            if (Activities.Count <= 0)
            {
                return;
            }

            Activity activity = Activities[0];

            ExecuteChildActivity(context, activity);
        }

        private void CompleteScope(NativeActivityContext context)
        {
            BusinessFailureExtension<T> extension = context.GetExtension<BusinessFailureExtension<T>>();

            IEnumerable<BusinessFailure<T>> businessFailures = extension.GetFailuresForScope(this);

            if (businessFailures == null)
            {
                return;
            }

            throw new BusinessFailureException<T>(businessFailures);
        }

        private void ExecuteChildActivity(NativeActivityContext context, Activity activity)
        {
            BusinessFailureExtension<T> extension = context.GetExtension<BusinessFailureExtension<T>>();

            extension.LinkActivityToScope(this, activity);

            context.ScheduleActivity(activity, OnActivityCompleted);
        }

        private void OnActivityCompleted(NativeActivityContext context, ActivityInstance completedInstance)
        {
            Int32 currentIndex = _activityExecutionIndex.Get(context);

            if ((currentIndex >= Activities.Count) || (Activities[currentIndex] != completedInstance.Activity))
            {
                currentIndex = Activities.IndexOf(completedInstance.Activity);
            }

            Int32 nextActivityIndex = currentIndex + 1;

            if (nextActivityIndex != Activities.Count)
            {
                Activity activity = Activities[nextActivityIndex];

                ExecuteChildActivity(context, activity);

                _activityExecutionIndex.Set(context, nextActivityIndex);
            }
            else
            {
                CompleteScope(context);
            }
        }

        [Browsable(false)]
        public Collection<Activity> Activities
        {
            get;
            set;
        }

        [Browsable(false)]
        public Collection<Variable> Variables
        {
            get;
            set;
        }
    }
}

The activity uses the CacheMetadata method to identify that it requires a BusinessFailureExtension<T> extension and provides the method to create one if it does not already exist. This method also configures the activity to support child activities and variable definitions.

The activity execution will resolve the extension instance before executing a child activity. It notifies the extension about the link between the scope and the child activity. The child activity can then use the extension to add a failure which the extension will store on behalf of the scope. The activity then gets the failures for the scope from the extension when it has executed all child activities. It then throws a BusinessFailureException<T> if there are any failures reported.

On a side note, the next version of this activity will refactor this last step so that the extension manages the exception throwing process as it does for BusinessFailureEvaluator<T>. This will align the code with the Single Responsibility Pattern so that only the extension understands how to handle failures and how to throw the BusinessFailureException<T>.

This post has provided the implementation for handling multiple business failures in a set. The next post will provide the designer implementation for these two activities.

Custom Workflow activity for business failure evaluation–Part 4

The previous post in this series provided the custom workflow extension that manages business failures. This post will provide a custom activity that evaluates a single business failure.

There are three important pieces of information to provide when defining a business failure with WF. These are the type of failure code, the failure condition and the failure details. The BusinessFailureEvaluator<T> activity supports these requirements in a compact way that makes authoring business failures easy in WF.

imageimage

The BusinessFailureEvaluator<T> activity is a generic activity type in order to support the generic type requirement of the Code property in BusinessFailure<T>. For ease of use, Int32 is the default type used for the generic type definition. The ArgumentType property can be used to change the activity use a different type for the Code property.

The activity defines a Nullable<Boolean> Condition property that drives whether the activity results in a failure at runtime. It also supports two methods of defining the failure details. These are to provide a BusinessFailure<T> instance in the Failure property or to define values for the Code and Description properties.

namespace Neovolve.Toolkit.Workflow.Activities
{
    using System;
    using System.Activities;
    using System.Activities.Presentation;
    using System.Activities.Validation;
    using System.ComponentModel;
    using System.Drawing;
    using Neovolve.Toolkit.Workflow.Extensions;
    using Neovolve.Toolkit.Workflow.Properties;
 
    [ToolboxBitmap(typeof(ExecuteBookmark), "shield.png")]
    [DefaultTypeArgument(typeof(Int32))]
    public sealed class BusinessFailureEvaluator<T> : CodeActivity where T : struct
    {
        protected override void CacheMetadata(CodeActivityMetadata metadata)
        {
            metadata.RequireExtension<BusinessFailureExtension<T>>();
            metadata.AddDefaultExtensionProvider(() => new BusinessFailureExtension<T>());

            Boolean conditionBound = Condition != null && Condition.Expression != null;

            if (conditionBound == false)
            {
                ValidationError validationError = new ValidationError(Resources.BusinessFailureEvaluator_NoConditionBoundWarning, true, "Condition");

                metadata.AddValidationError(validationError);
            }

            Boolean failureBound = Failure != null && Failure.Expression != null;
            Boolean codeBound = Code != null && Code.Expression != null;
            Boolean descriptionBound = Description != null && Description.Expression != null;
            const String CodePropertyName = "Code";

            if (failureBound == false && codeBound == false && descriptionBound == false)
            {
                ValidationError validationError = new ValidationError(
                    Resources.BusinessFailureEvaluator_FailureInformationNotBound, false, CodePropertyName);

                metadata.AddValidationError(validationError);
            }
            else if (failureBound && (codeBound || descriptionBound))
            {
                ValidationError validationError = new ValidationError(
                    Resources.BusinessFailureEvaluator_ConflictingFailureInformationBound, false, CodePropertyName);

                metadata.AddValidationError(validationError);
            }
            else if (codeBound && descriptionBound == false)
            {
                ValidationError validationError = new ValidationError(Resources.BusinessFailureEvaluator_DescriptionNotBound, false, "Description");

                metadata.AddValidationError(validationError);
            }
            else if (codeBound == false && descriptionBound)
            {
                ValidationError validationError = new ValidationError(Resources.BusinessFailureEvaluator_CodeNotBound, false, CodePropertyName);

                metadata.AddValidationError(validationError);
            }

            base.CacheMetadata(metadata);
        }

        protected override void Execute(CodeActivityContext context)
        {
            Nullable<Boolean> condition = Condition.Get(context);

            if (condition != null && condition == false)
            {
                // This is not a failure
                return;
            }

            BusinessFailure<T> failure = Failure.Get(context);

            if (failure == null)
            {
                T code = Code.Get(context);
                String description = Description.Get(context);

                failure = new BusinessFailure<T>(code, description);
            }

            BusinessFailureExtension<T> extension = context.GetExtension<BusinessFailureExtension<T>>();

            extension.ProcessFailure(this, failure);
        }

        [Category("Inputs")]
        [Description("The code of the failure")]
        public InArgument<T> Code
        {
            get;
            set;
        }

        [Category("Inputs")]
        [Description("The condition used to determine if there is a failure")]
        public InArgument<Nullable<Boolean>> Condition
        {
            get;
            set;
        }

        [Category("Inputs")]
        [Description("The description of the failure")]
        public InArgument<String> Description
        {
            get;
            set;
        }

        [Category("Inputs")]
        [Description("The failure")]
        public InArgument<BusinessFailure<T>> Failure
        {
            get;
            set;
        }
    }
}

The CacheMetadata method identifies that the activity requires a BusinessFailureExtension<T> extension and provides a default function to create one if it does not yet exist. The remainder of the method defines a set of validation rules for the activity. If no Condition is bound then the designer will display a warning message on the activity to indicate that this will always result in an exception.

image

The validation logic then checks for errors with the configuration of the activity. A Failure value or Code and Description must be provided and these are values are mutually exclusive. If a Code has been bound then a Description must also be bound and visa versa. Like the above warning, these validation errors are displayed in the designer surface however they also result in an exception thrown by the workflow engine if the activity is executed without addressing these errors.

image

The Execute method of the activity is where all the runtime processing occurs.

The only way that the activity will not result in an exception is if a Condition expression has been provided and its runtime value is false. Not binding the Condition property, providing a null value or providing a value of true means that a failure will be processed by the extension. The ability to not bind a Condition expression is useful where prior WF processing (such as an If statement) has already processed the conditional logic for the failure.

The activity will resolve a BusinessFailure<T> instance once the Execute method has determined that a business failure has been encountered. It first looks for a Failure property instance and will then create a new instance with Code and Description properties if it does not find a failure instance. The failure instance is then passed on to the extension for processing.

This post has shown how to create a simple business failure evaluation activity with validation support. The next post will look at how to provide the ability to group a set of failures using a composite activity.

Custom Workflow activity for business failure evaluation–Part 3

The previous post looked at the support for creating business failures and throwing the failures with an exception. This post will look at a custom WF extension that will process business failures in WF.

Using a custom extension to manage business failures abstracts workflow activities from the logic of processing business failures. The extension is responsible for processing business failures as they are reported by activities. It will also need to track relationships between activities in order to support the design goal of grouping related business failures into a single exception. This is achieved by providing a way of linking activities.

The Activity class in WF 3.0 and 3.5 provided a public Activity Parent property that could be used to traverse up the activity tree hierarchy. Unfortunately this property has been marked as internal in WF 4.0. Reflection could be used to get around this restriction but it is a hack at best. This prevents an automated method of walking up the workflow activity hierarchy. Similarly, inspecting child activity hierarchies is unreliable as there is no standard method for exposing or identifying child activities even if they are publicly available on an activity type. This prevents automated discovery of child activities. In addition to these issues, the automatic detection of activity hierarchies for linking activities may produce unintended results as activities are linked when they were not expected to be.

The alternative to these automated methods is to implement an explicit opt-in design where a parent activity informs the extension about a link to a child activity. This makes the parent activity responsible for informing the extension about a link to a child activity. The extension then uses this knowledge in the processing of the business failure.

namespace Neovolve.Toolkit.Workflow.Extensions
{ 
    using System;
    using System.Activities;
    using System.Activities.Persistence;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Diagnostics;
    using System.Diagnostics.Contracts;
    using System.Linq;
    using System.Threading;
    using System.Xml.Linq;
    using Neovolve.Toolkit.Threading;

    public class BusinessFailureExtension<T> : PersistenceParticipant, IDisposable where T : struct
    {
        private static readonly XNamespace _persistenceNamespace = XNamespace.Get("http://www.neovolve.com/toolkit/workflow/properties");

        private static readonly XName _scopeEvaluatorsName = _persistenceNamespace.GetName("ScopeEvaluators");

        private static readonly XName _scopeFailuresName = _persistenceNamespace.GetName("ScopeFailures");

        private readonly ReaderWriterLockSlim _scopeEvaluatorsLock = new ReaderWriterLockSlim();

        private readonly ReaderWriterLockSlim _scopeFailuresLock = new ReaderWriterLockSlim();

        private Dictionary<String, String> _scopeEvaluators = new Dictionary<String, String>();

        private Dictionary<String, Collection<BusinessFailure<T>>> _scopeFailures = new Dictionary<String, Collection<BusinessFailure<T>>>();

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        public IEnumerable<BusinessFailure<T>> GetFailuresForScope(Activity scopeActivity)
        {
            Contract.Requires<ArgumentNullException>(scopeActivity != null);
            Contract.Requires<ArgumentException>(String.IsNullOrEmpty(scopeActivity.Id) == false);

            String activityId = scopeActivity.Id;

            RemoveActivitiesLinkedToScope(activityId);

            using (new LockWriter(_scopeFailuresLock))
            {
                if (_scopeFailures.ContainsKey(activityId))
                {
                    Collection<BusinessFailure<T>> failures = _scopeFailures[activityId];

                    _scopeFailures.Remove(activityId);

                    return failures;
                }
            }

            return null;
        }

        public Boolean IsLinkedToScope(Activity activity)
        {
            Contract.Requires<ArgumentNullException>(activity != null);
            Contract.Requires<ArgumentException>(String.IsNullOrEmpty(activity.Id) == false);

            String activityId = activity.Id;

            String scopeActivityId = GetOwningScopeId(activityId);

            if (String.IsNullOrWhiteSpace(scopeActivityId))
            {
                return false;
            }

            return true;
        }

        public void LinkActivityToScope(Activity scopeActivity, Activity childActivity)
        {
            Contract.Requires<ArgumentNullException>(scopeActivity != null);
            Contract.Requires<ArgumentNullException>(childActivity != null);
            Contract.Requires<ArgumentException>(String.IsNullOrEmpty(scopeActivity.Id) == false);
            Contract.Requires<ArgumentException>(String.IsNullOrEmpty(childActivity.Id) == false);

            using (new LockWriter(_scopeEvaluatorsLock))
            {
                _scopeEvaluators[childActivity.Id] = scopeActivity.Id;
            }
        }

        public void ProcessFailure(Activity activity, BusinessFailure<T> failure)
        {
            Contract.Requires<ArgumentNullException>(activity != null);
            Contract.Requires<ArgumentException>(String.IsNullOrEmpty(activity.Id) == false);
            Contract.Requires<ArgumentNullException>(failure != null);

            String activityId = activity.Id;

            String scopeActivityId = GetOwningScopeId(activityId);

            if (String.IsNullOrEmpty(scopeActivityId))
            {
                // There is no scope activity that contains this evaluator
                throw new BusinessFailureException<T>(failure);
            }

            using (new LockWriter(_scopeFailuresLock))
            {
                Collection<BusinessFailure<T>> failures;

                if (_scopeFailures.ContainsKey(scopeActivityId))
                {
                    failures = _scopeFailures[scopeActivityId];
                }
                else
                {
                    failures = new Collection<BusinessFailure<T>>();

                    _scopeFailures.Add(scopeActivityId, failures);
                }

                // Store the failure for the scope
                failures.Add(failure);
            }
        }

        protected override void CollectValues(out IDictionary<XName, Object> readWriteValues, out IDictionary<XName, Object> writeOnlyValues)
        {
            Dictionary<String, String> evaluators;

            using (new LockReader(_scopeEvaluatorsLock))
            {
                evaluators = new Dictionary<String, String>(_scopeEvaluators);
            }

            Dictionary<String, Collection<BusinessFailure<T>>> scopeFailures;

            using (new LockReader(_scopeFailuresLock))
            {
                scopeFailures = new Dictionary<string, Collection<BusinessFailure<T>>>(_scopeFailures);
            }

            readWriteValues = new Dictionary<XName, Object>
                              {
                                  {
                                      _scopeEvaluatorsName, evaluators
                                      }, 
                                  {
                                      _scopeFailuresName, scopeFailures
                                      }
                              };

            writeOnlyValues = null;
        }

        protected virtual void Dispose(Boolean disposing)
        {
            if (disposing)
            {
                // Free managed resources
                if (Disposed == false)
                {
                    Disposed = true;

                    _scopeEvaluatorsLock.Dispose();
                    _scopeFailuresLock.Dispose();
                }
            }

            // Free native resources if there are any.
        }

        protected override void PublishValues(IDictionary<XName, Object> readWriteValues)
        {
            base.PublishValues(readWriteValues);

            Object evaluators;

            if (readWriteValues.TryGetValue(_scopeEvaluatorsName, out evaluators))
            {
                using (new LockWriter(_scopeEvaluatorsLock))
                {
                    _scopeEvaluators = (Dictionary<String, String>)evaluators;
                }
            }

            Object failures;

            if (readWriteValues.TryGetValue(_scopeFailuresName, out failures))
            {
                using (new LockWriter(_scopeFailuresLock))
                {
                    _scopeFailures = (Dictionary<String, Collection<BusinessFailure<T>>>)failures;
                }
            }
        }

        private String GetOwningScopeId(String activityId)
        {
            Debug.Assert(String.IsNullOrEmpty(activityId) == false, "No activity id provided");

            using (new LockReader(_scopeEvaluatorsLock))
            {
                if (_scopeEvaluators.ContainsKey(activityId))
                {
                    return _scopeEvaluators[activityId];
                }
            }

            return null;
        }

        private void RemoveActivitiesLinkedToScope(String scopeActivityId)
        {
            List<String> evaluatorIds = new List<String>();

            using (new LockReader(_scopeEvaluatorsLock))
            {
                evaluatorIds.AddRange(
                    from valuePair in _scopeEvaluators
                    where valuePair.Value == scopeActivityId
                    select valuePair.Key);
            }

            using (new LockWriter(_scopeEvaluatorsLock))
            {
                evaluatorIds.ForEach(x => _scopeEvaluators.Remove(x));
            }
        }

        protected Boolean Disposed
        {
            get;
            set;
        }
    }
}

The BusinessFailureExtension exposes a LinkActivityToScope method that creates the link between a scope and child activity. Child activities can check if they are linked to a scope by calling the IsLinkedToScope method. The link between these activities uses a Dictionary<String, String> instance to store the associations. The key of the dictionary is the ActivityId of the linked activity and the value is the ActivityId of the scope activity. This design allows for multiple activities to be linked to a scope while enforcing that an activity is only linked to a single scope activity.

The extension defines a ProcessFailure method for processing failures provided by an activity. The extension will throw a BusinessFailureException<T> straight away if the method does not find a link between the failure activity and a scope activity. The failure is stored in a failure list associated with the scope activity if there is a link found with a scope activity.

The GetFailuresForScope method returns any failures stored against a scope activity. This method returns the collection of failures that have been stored against a scope activity when a linked activity has invoked ProcessFailure. This method also cleans up stored information for the scope by removing any links to other activities and removing the failures stored for it.

The extension must support workflow persistence. This caters for the scenario where a linked activity has stored a failure against a scope activity and the workflow is persisted before the scope activity invokes GetFailuresForScope.

image

Persistence is supported by inheriting from PersistenceParticipant and overriding CollectValues and PublishValues. The CollectValues method provides the activity links and stored failures to the persistence process. The PublishValues method then restores these values again when the workflow is rehydrated from the persistence store.

Lastly, the extension implements IDisposable to ensure that ReaderWriterLock instances are disposed. The workflow execution engine will dispose the extension when the workflow execution has completed.

This post has demonstrated how a custom extension can be used by any activity to work with business failures. The next post will look at the custom activity for evaluating a business failure.

Custom Workflow activity for business failure evaluation–Part 2

The previous post provided the high level design requirements for a custom WF activity that evaluates business failures. This post will provide the base classes that will support the custom WF activity.

The first issue to work on is how to express a business failure. The design requirements indicated that a business failure needs to identify a code and a description. The design also defined that the code value must be generic in order to avoid placing an implementation constraint on the consuming application. 

namespace Neovolve.Toolkit.Workflow
{
    using System;
    using System.Diagnostics.Contracts;
    using Neovolve.Toolkit.Workflow.Properties;

    [Serializable]
    public class BusinessFailure<T> where T : struct
    {
        public BusinessFailure(T code, String description)
        {
            Contract.Requires<ArgumentNullException>(String.IsNullOrEmpty(description) == false);

            Code = code;
            Description = description;
        }

        public static BusinessFailure<T> UnknownFailure
        {
            get
            {
                return new BusinessFailure<T>(default(T), Resources.BusinessFailure_UnknownFailure);
            }
        }

        public T Code
        {
            get;
            private set;
        }

        public String Description
        {
            get;
            private set;
        }
    }
}

The BusinessFailure<T> class supports these design goals. There is a constraint defined for the code type T that enforces the type to be a struct. Primarily this is to ensure that the code value is serializable and enforces failure codes to be simple types. One thing to note about this class is that it is marked as Serializable. This is critically important in order to support scenarios where a business failure has been created but not yet processed before the executing workflow is persisted.

The next part of the design to address is how a business failure gets processed. The design describes that this will be done using a custom exception. The exception allows calling applications have access to all the failures related to an exception and enforces applications to leverage structured error handling practises to process business failures.

namespace Neovolve.Toolkit.Workflow
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Globalization;
    using System.Runtime.Serialization;
    using Neovolve.Toolkit.Workflow.Properties;

    [Serializable]
    public class BusinessFailureException<T> : Exception where T : struct
    {
        private const String FailuresKey = "FailuresKey";

        private const String IncludeBaseMessageKey = "IncludeBaseMessageKey";

        public BusinessFailureException()
        {
            IncludeBaseMessage = false;
            Failures = new Collection<BusinessFailure<T>>();
        }

        public BusinessFailureException(String message)
            : base(message)
        {
            IncludeBaseMessage = true;
            Failures = new Collection<BusinessFailure<T>>();
        }

        public BusinessFailureException(T code, String description)
        {
            IncludeBaseMessage = false;
            BusinessFailure<T> failure = new BusinessFailure<T>(code, description);

            Failures = new Collection<BusinessFailure<T>>
                       {
                           failure
                       };
        }

        public BusinessFailureException(BusinessFailure<T> failure)
        {
            IncludeBaseMessage = false;
            Failures = new Collection<BusinessFailure<T>>
                       {
                           failure
                       };
        }

        public BusinessFailureException(IEnumerable<BusinessFailure<T>> failures)
        {
            IncludeBaseMessage = false;
            Failures = new List<BusinessFailure<T>>(failures);
        }

        public BusinessFailureException(String message, Exception inner)
            : base(message, inner)
        {
            IncludeBaseMessage = true;
            Failures = new Collection<BusinessFailure<T>>();
        }

        protected BusinessFailureException(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
            IncludeBaseMessage = info.GetBoolean(IncludeBaseMessageKey);
            Failures = (ICollection<BusinessFailure<T>>)info.GetValue(FailuresKey, typeof(Collection<BusinessFailure<T>>));
        }

        public override void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue(IncludeBaseMessageKey, IncludeBaseMessage);
            info.AddValue(FailuresKey, Failures);

            base.GetObjectData(info, context);
        }

        public IEnumerable<BusinessFailure<T>> Failures
        {
            get;
            private set;
        }

        public override String Message
        {
            get
            {
                String message = String.Empty;

                if (IncludeBaseMessage)
                {
                    message = base.Message;
                }

                String failureMessages = String.Empty;

                List<BusinessFailure<T>> businessFailures = new List<BusinessFailure<T>>(Failures);

                if (businessFailures.Count == 0)
                {
                    // Add a default failure
                    businessFailures.Add(BusinessFailure<T>.UnknownFailure);
                }

                for (Int32 index = 0; index < businessFailures.Count; index++)
                {
                    BusinessFailure<T> failure = businessFailures[index];
                    String failureMessage = String.Format(
                        CultureInfo.CurrentUICulture, Resources.BusinessFailureException_FailureMessageFormat, failure.Code, failure.Description);

                    failureMessages = String.Concat(failureMessages, Environment.NewLine, failureMessage);
                }

                message += String.Format(CultureInfo.CurrentUICulture, Resources.BusinessFailureException_MessageHeader, failureMessages);

                return message;
            }
        }

        protected Boolean IncludeBaseMessage
        {
            get;
            set;
        }
    }
}

The code analysis rules provided by Microsoft defines the bulk of the signatures on this type. This is done to provide a consistent exception implementation for consumers. The business failure specific parts of this implementation are in three areas:

  1. Exception construction with business failure information
  2. Custom Message property generation to include failure information
  3. Access to the set of BusinessFailure<T> entities
  4. Serialization

The custom Message property is important for human readable scenarios, such as output to a UI or writing the exception to a log entry. Exposing the set of BusinessFailure<T> instances is important for automated referencing, such as associating UI field failure indicators using the failure code as the link.

The next post in the series will provide the implementation of a WF activity that evaluates and processes a business failure.

Custom Workflow activity for business failure evaluation–Part 1

Following on from my series about custom WF support for dependency resolution, this series of posts will look at support for business failure evaluation.

Every project I have worked on has some kind of requirement to execute data validation and/or business rules when running a business process. Data validation could be a test that a request message contains an email address whereas a business rule may be that the provided email address is unique. Providing a custom WF activity to facilitate validation allows for rapid development of business processes.

The high-level designer requirements for the custom WF support are:

  • Failure identifies a code and a description
  • Failure evaluation supports conditional expressions
  • Code value must be a generic type
  • Failures are thrown in an exception
  • Exception must support multiple failures
  • Adequate design time support

The remainder of this post will outline the reasons behind these requirements.

Failure identifies a code and a description

Descriptions are strings that often provide detailed information about a failure. Unfortunately descriptions are not sufficient as a reference point for taking action based on the failure.

Consider a UI that invokes a component that in turn throws a failure exception. The UI could provide a good user experience by identifying the input field on the form that relates to the failure. Matching failure descriptions to achieve this is fragile because different culture settings may result in different descriptions that are logically the same but are actually different as far as strings are concerned.

Using a code value is a culture independent way of identifying the failure. A culture aware application can identify a UI field related to the culture agnostic code and display the culture aware failure description for the user.

Failure evaluation supports conditional expressions

The custom activity should support evaluating a conditional expression that determines whether the activity is going to result in a failure. This is important to prevent the developer having to always surround the custom activity with If statements that run the business rules for the failure.

Code value must be a generic type

The custom activity is a common reusable workflow activity. As such it cannot know what data type is going to be used to represent the code value. One application may use integer codes whereas another may use Guid or string values. Defining the failure with a generic code allows the developer to use a code value that is suitable to their requirements.

Failures are thrown in an exception

Business failures in this design are intended to be exceptional circumstances that are handled within the structured error handling design of .Net. The custom activity support should therefore throw failures up the call stack using an exception.

Exception must support multiple failures

Business validation is very different to system validation. System validation tends to be single issue identification whereas business validation often identifies multiple issues.

Consider a registration request where the request contains FirstName, LastName and Email. The business validation rules for such a request may be that all fields must be provided. The system would provide a poor user experience if every submission of the registration request resulted in a new failure message. This process can be streamlined by providing one exception on the first attempt that identified all three validation failures.

Adequate design time support

Any custom workflow activity should provide an adequate design time experience. This custom activity support should allow the developer to manage all aspects of the failure evaluation process through the designer experience.

The next post will look at the base framework for supporting business validation.