Introduction to Rule Engine

In FIM 2010 Microsoft introduced Synchronization Rules in the Portal. Using the Portal GUI you can chain function calls in a simple DSL (domain specific language). These rules then get imported to the metaverse and run when objects synchronize.

The advantage of portal rules is that they allow you to define rules to do simple transformations and control flow without code that would normally require a rules extension. For example, it’s much easier to set up import rules with a GUI configured Trim wrapper than to add extension rules with a separate switch case for every attribute you want to trim. On a large project, the reduction in code can be quite impressive. There are some caveats with Microsoft’s implementation though:

  • Because the available functions aren’t extensible, you almost always end up needing to write a rules extension anyway to handle more complex flows, and you can’t combine the simple portal rules with more complex custom code.
  • It’s tricky to manage changes because Portal Rules may be edited by anyone with administrative portal access. Also, as database objects (rather than code or configuration files, it’s difficult to manage changes to keep things consistent, like you can with a Rules Extension attribute flow.
  • Because the flow rules live in the database and are tied to the execution environment by GUID, it is more challenging to keep rules unchanged and consistent between testing, validation, and production environments. The ExampleExtension library provides an example of how to import the synchronization rule DSL that may be used in your own rules extensions. You can place the DSL logic (that would normally be edited on the portal) in the attribute flow rule name of an advanced attribute flow, and it will be automatically parsed and executed.

The ExampleExtension also shows how RuleEngine functionality can be extended with custom functions to provide additional functionality.

One approach that can be taken for complex rules is to use the DSL to handle logic for whether to perform an attribute flow, and use custom code for how to construct the values.

Flow Rule DSL

Flow Rules take the form: Rule -> targetAttribute this says that Rule will be executed, and the resulting value will be assigned to targetAttribute on the csentry or mventry that is the recipient of the export or import flow. For clarity, targetAttribute optionally may be fully qualified (e.g. csentry.targetAttribute).

All values produced in Rules will be automatically type converted where possible e.g. parsing “42” to 42 to assign an integer value. The type converting logic is quite similar to javascript type coercion, except that it will also handle a FIM ReferenceValue.

Rule Types

Rules may take the following forms, and may be nested to arbitrary depth.

  • A Rule may be a C# style Function Call
    • e.g. Function(params Rule[] vals)
  • A Rule may be C# style literal (supports String, Boolean, and Long)
    • E.g. “String Example”, true, -35
  • A Rule may concatenate other rules as strings
    • E.g. Rule + Rule + Rule…
  • A Rule may use a comparison operator to express a Boolean
    • E.g. IIF(userAccountControl == 512, …, accepts <, >, <=, >=, ==
  • A Rule may be a csentry or mventry attribute reference
    • E.g. displayName, csentry.displayname, mventry.givenName
    • If the field name is provided without a qualifying csentry or mventry, it is assumed to be on the source object, i.e. the csentry object for an Import Flow Rule, and the mventry object for an Export Flow Rule. If the value is qualified, then values from the Flow Rule target may be referenced. E.g. If an import rule needs to verify that a field is null before writing to it, you might write IIF(IsNullOrEmpty(mventry.sAMAcccountName)…,
    • It is valid to use special case attributes in a rule, such as csentry.DN.
    • A Rule may reference a whole csentry or mventry object (but only as an argument to a function rule)
    • E.g. CustomFunction(csentry)
    • In some cases to eliminate a long list of function parameters, or to otherwise simplify things, it is more convenient just to pass the whole csentry} or mventry object as an argument to a custom function. This is especially true when constructing a value from multiple fields as you might with displayName or sAMAccountName. Note: when passing a whole [cs/mv]entry object to a function, the parameter in the function definition should have type EntryWrapper instead of AttributeValue.

CommonFunctions

Uplift includes implementations of the method signatures available as part of a FIM Portal Synchronization Rule. These functions are implemented and documented in CommonFunctions.cs. CommonFunctions also defines a few other useful functions, all of which may be referenced in the attribute Flow rule name logic. The basic logic of most functions in CommonFunctions is similar to the portal rule methods described at http://technet.microsoft.com/en-us/library/ff800820%28WS.10%29.aspx. Additional functions are described below.

And(params[] value) Combines Boolean parameters with &&. Implements short-circuit evaluation, so it returns after the first false argument.

Or(params[] value Combines Boolean parameters with ||. Implements short-circuit evaluation, so it returns after the first true argument.

Not(value) Negates a Boolean value

Skip() Skips flowing any value

RuleEngine Classes

The RuleEngine class converts the FlowRuleName from FIM into a Rule object, executes the rule, and writes the result to the csentry or mventry object.

The RuleEngine class is the only part of Uplift Rule support that must be referenced directly from your Extension code. The RuleEngine constructor takes a params array of Type objects that it then reflects to find static functions to make available for rules.

Instantiating the RuleEngine

Signature of RuleEngine constructor:

public RuleEngine(params Type[] types)

When instantiating the RuleEngine, you should pass it references to the classes containing functions that should be available to rules.

public class ExampleExtension : IMASynchronization
{
    /// <summary>
    /// The RuleEngine is the core class for Rule based attribute flow.
    /// </summary>
    RuleEngine rules = new RuleEngine(typeof(CommonFunctions), typeof(ExampleFunctions));

Instantiating the RuleEngine in the Rules Extension (IMASynchronization class) should be done in either the constructor or the IMASynchronization.Initialize method. This improves performance by allowing the same instance to be used for all rules and records. It also allows the Changes hologram to function.

Rule Hologram, ChangeFlowEnd Event, and ChangeSet

The Rule Hologram and ChangeSet is used to track changes to an object during attribute flow and allows notifications and logging to be placed in one event handler rather than scattered through the extension. The values of the Hologram and ChangeSet are designed to be used in conjunction with the MailTemplate to construct customized notification emails.

ChangeHoloFields

After the RuleEngine is instantiated, in either the constructor or Initialize method, assign the ChangeHoloFields property. This instructs the RuleEngine which fields to cache hologram values for. Specifying a limited set of fields improves performance and memory usage.

e.g.

rules.ChangeHoloFields = new Dictionary<string, string[]>() { 
    { "user", new string[] { "employeeID", "displayName", "sAMAccountName", "department", "st" } } 
};

ChangeHoloFields is a Dictionary of string arrays. The keys in the dictionary should be the object type to track, and the array should be a list of fields.

It is important to note that the Hologram represents the values before synchronization on the target object. For example, on an import that sets “sAMAccountName” from csentry.user to mventry.user, the hologram object will cache the original value of mventry.user[“sAMAccountName”].

After a single object finishes synchronizing, the RuleEngine will invoke the ChangeFlowEnd event. By handling this event you can create log entries and messages using values passed in the ChangeSet object.

ChangeSet

The ChangeSet object has two properties:

  • Hologram: the original values of fields on the target for the objects and attributes specified in ChangeHoloFields
  • Changes: A dictionary of Change objects containing every field that has been affected by an Uplift flow rule.

Change

The Change object has:

  • Direction: whether the change corresponds to an import or export flow rule
  • Field: the name of the target field written to
  • OldValue: an AttributeValue representing the original state of the field
  • NewValue: an AttributeValue representing the updated value

Rule Execution

The first time a synchronization rule is processed the RuleEngine constructs a rule instance and caches it. This way the rule only has to be parsed once. Later invocations are made using a fast dictionary lookup.

Once the RuleEngine object is present, it can be used with identical syntax from both MapAttributesForImport and MapAttributesForExport.

public virtual void MapAttributesForImport(string FlowRuleName, CSEntry csentry, MVEntry mventry)
{
    try
    {
        rules.Run(FlowRuleName, csentry, mventry);
    }
    catch (Exception e)
    {
        LogWriter.writeEntry("FIM", "Import Error", "ExampleExtension", LogType.Error, 
            "FlowRule: {0}\r\nError: {1}\r\nTrace: {2}", FlowRuleName, e.Message, e.StackTrace);
    }
}

AttributeValue

The AttributeValue class represents a wrapper that produces a value in a flow rule.

Because AttributeValue represents the results of executing Rules, an AttributeValue is actually a common interface that unifies a number of value concepts. This allows values from different kinds of Rule to be used interchangeably.

  • An AttributeValue is what’s returned by new EntryWrapper(csentry).Get(“fieldName”) or {{{new EntryWrapper(csentry)["fieldName"]. In other words, it can represent the value of a field on a csentry or mventry object.
  • The class AttributeValue is the return type of any function that is included in a flow rule. This allows Rules to use a common language to handle attribute values with different underlying data types.
  • All the arguments of a flow rule function must also be of type AttributeValue. If you look through CommonFunctions.cs, you’ll see that function parameter and return types are always AttributeValue (the one exception to this is that whole csentry and mventry arguments are passed as EntryWrapper objects).
  • AttributeValue is a type agnostic wrapper for string, bool, byte[], long, ReferenceValue, and ValueCollection values. This allows values to be read and assigned with automatic type conversion and with much less concern for type errors and null reference exceptions.

For example: AttributeValue CommonFunctions.ConvertSidToString(AttributeValue sid) is a wrapper for string Microsoft.MetadirectoryServices.Utils.ConvertSidToString(byte[] sid)

  • An AttributeValue instance can be an unbound wrapper for a Rule. Because some Rules may have undesired side-effects (such as writing log entries) if they are executed prematurely, an AttributeValue can be constructed around a Rule, so that accessing the value of the AttributeValue performs lazy evaluation of the Rule to produce the value. For example, if you have a Rule with the form: IIF(TrueCondition(), SomeFunction(), SideEffectFunction()), only TrueCondition() and SomeFunction() will be called. SideEffectFunction() is never accessed.

Rule

The Rule class is the core of synchronization rule parsing and execution. It consists of three basic parts.

  • The constructor tokenizes the Rule input string to generate a parse tree.
  • The GetValue method returns a lazily bound AttributeValue (which is just a wrapper with a reference back to the rule). This AttributeValue may be used anywhere values need to be assigned or read, but actual evaluation of the Rule is deferred until a Value property of the AttributeValue accessed. This is important because it allows functions with side effects to be used in branching conditionals (see AttributeValue).
  • The LateBind method is a callback used by the unbound AttributeValue that was constructed around the Rule. This method actually executes the rule to obtain the value.

EntryWrapper

The EntryWrapper class is used primarily for convenience and code reuse. In order to have uniform code for import and export attribute flows, it is necessary to wrap the CSEntry and MVEntry objects in a common interface.

Both Microsoft.MetaDirectoryServices.CSentry and MVentry support many of the same operations, particularly with regard to reading and writing attributes. Unfortunately, they don’t inherit from a common base class, which means code can’t be reused for both export and import attribute flow.

An EntryWrapper object may be constructed around a csentry or mventry object, allowing it to be included in both import and export flow rules without worrying about the underlying object type. The EntryWapper class also allows cleaner code to be written by providing a few helpful features.

  • Accessing an attribute (by calling public AttributeValue Get(string field)) will return null/false/0 if the attribute is not present. This means that attributes may be accessed safely without obsessively invoking cs/mventry[field].IsPresent.
  • When assigning a null value to a cs/mventry field, EntryWrapper will automatically delete the attribute. This avoids the common problem of code being inconsistent in deleting a field versus writing null to it.
  • The EntryWrapper will automatically allow the special case fields DN, RDN, and objectType to be accessed with the same case-insensitive syntax as regular fields, allowing these fields to be used normally in declarative flow rules.
  • When a field is assigned using the Set method, the EntryWrapper will do its best to perform automatic type conversion. For example "35" can be assigned as an integer, and false may be read as the string "false".

Copyright © SoftwareIDM