Skip to content

Commit

Permalink
Action triggers #161
Browse files Browse the repository at this point in the history
- Add a notion of action triggers, that indicate how the activation was
triggered (create, update, removal)
- Ability to associate expressions with individual action triggers.
Including expressions on action removals
- Changed how linked facts are managed, to leverate action triggers
- Tests
  • Loading branch information
snikolayev committed Jun 3, 2018
1 parent 7b77cf4 commit 911d3ff
Show file tree
Hide file tree
Showing 20 changed files with 1,141 additions and 118 deletions.
32 changes: 31 additions & 1 deletion src/NRules/NRules.Fluent/Dsl/IRightHandSideExpression.cs
Expand Up @@ -10,12 +10,29 @@ namespace NRules.Fluent.Dsl
public interface IRightHandSideExpression
{
/// <summary>
/// Defines rule's action that engine executes when the rule fires.
/// Defines rule's action that engine executes for a given trigger.
/// </summary>
/// <param name="action">Action expression.</param>
/// <param name="actionTrigger">Events that should trigger this action.</param>
/// <returns>Right hand side expression builder.</returns>
IRightHandSideExpression Action(Expression<Action<IContext>> action, ActionTrigger actionTrigger);

/// <summary>
/// Defines rule's action that engine executes when the rule fires
/// due to the initial rule match or due to an update.
/// </summary>
/// <param name="action">Action expression.</param>
/// <returns>Right hand side expression builder.</returns>
IRightHandSideExpression Do(Expression<Action<IContext>> action);

/// <summary>
/// Defines rule's action that engine executes when the rule fires
/// due to the match removal (provided the rule previously fired on the match).
/// </summary>
/// <param name="action">Action expression.</param>
/// <returns>Right hand side expression builder.</returns>
IRightHandSideExpression Undo(Expression<Action<IContext>> action);

/// <summary>
/// Defines rule's action that yields a linked fact when the rule fires.
/// If the rule is fired due to an update, the linked fact is also updated with the new yielded value.
Expand All @@ -34,5 +51,18 @@ public interface IRightHandSideExpression
/// <param name="yieldUpdate">Action expression that yields an updated linked fact if the linked fact already exists.</param>
/// <returns>Right hand side expression builder.</returns>
IRightHandSideExpression Yield<TFact>(Expression<Func<IContext, TFact>> yieldInsert, Expression<Func<IContext, TFact, TFact>> yieldUpdate);

/// <summary>
/// Defines rule's action that yields a linked fact when the rule fires.
/// If the rule is fired due to an update, the update expression is evaluated to produce an updated linked fact.
/// If the rule's conditions are no longer true and the rule's activation is removed, the remove expression is evaluated and the linked fact is retracted.
/// </summary>
/// <typeparam name="TFact">Type of fact to yield.</typeparam>
/// <param name="yieldInsert">Action expression that yields a new linked fact if the linked fact does not yet exist.</param>
/// <param name="yieldUpdate">Action expression that yields an updated linked fact if the linked fact already exists.</param>
/// <param name="yieldRemove">Action expression that is evaluated before the linked fact is retracted.</param>
/// <returns>Right hand side expression builder.</returns>
IRightHandSideExpression Yield<TFact>(Expression<Func<IContext, TFact>> yieldInsert, Expression<Func<IContext, TFact, TFact>> yieldUpdate,
Expression<Action<IContext, TFact>> yieldRemove);
}
}
4 changes: 2 additions & 2 deletions src/NRules/NRules.Fluent/Expressions/BuilderExtensions.cs
Expand Up @@ -35,11 +35,11 @@ public static void DslBindingExpression(this BindingBuilder builder, IEnumerable
builder.BindingExpression(rewrittenExpression);
}

public static void DslAction(this ActionGroupBuilder builder, IEnumerable<Declaration> declarations, Expression<Action<IContext>> action)
public static void DslAction(this ActionGroupBuilder builder, IEnumerable<Declaration> declarations, Expression<Action<IContext>> action, ActionTrigger actionTrigger)
{
var rewriter = new ExpressionRewriter(declarations);
var rewrittenAction = rewriter.Rewrite(action);
builder.Action(rewrittenAction);
builder.Action(rewrittenAction, actionTrigger);
}

public static LambdaExpression DslPatternExpression(this PatternBuilder builder, IEnumerable<Declaration> declarations, LambdaExpression expression)
Expand Down
95 changes: 57 additions & 38 deletions src/NRules/NRules.Fluent/Expressions/RightHandSideExpression.cs
Expand Up @@ -17,71 +17,90 @@ public RightHandSideExpression(RuleBuilder builder)
_builder = builder;
}

public IRightHandSideExpression Do(Expression<Action<IContext>> action)
public IRightHandSideExpression Action(Expression<Action<IContext>> action, ActionTrigger actionTrigger)
{
var rightHandSide = _builder.RightHandSide();
rightHandSide.DslAction(rightHandSide.Declarations, action);
rightHandSide.DslAction(rightHandSide.Declarations, action, actionTrigger);
return this;
}

public IRightHandSideExpression Do(Expression<Action<IContext>> action)
{
return Action(action, ActionTrigger.Activated | ActionTrigger.Reactivated);
}

public IRightHandSideExpression Undo(Expression<Action<IContext>> action)
{
return Action(action, ActionTrigger.Deactivated);
}

public IRightHandSideExpression Yield<TFact>(Expression<Func<IContext, TFact>> yield)
{
_linkedCount++;
var context = yield.Parameters[0];
var linkedFact = Expression.Parameter(typeof(TFact));
var yieldUpdate = Expression.Lambda<Func<IContext, TFact, TFact>>(yield.Body, context, linkedFact);
return Yield(yield, yieldUpdate);
}

public IRightHandSideExpression Yield<TFact>(Expression<Func<IContext, TFact>> yieldInsert, Expression<Func<IContext, TFact, TFact>> yieldUpdate)
{
var yieldRemove = Expression.Lambda<Action<IContext, TFact>>(Expression.Empty(), yieldUpdate.Parameters);
return Yield(yieldInsert, yieldUpdate, yieldRemove);
}

public IRightHandSideExpression Yield<TFact>(Expression<Func<IContext, TFact>> yieldInsert, Expression<Func<IContext, TFact, TFact>> yieldUpdate, Expression<Action<IContext, TFact>> yieldRemove)
{
_linkedCount++;
var linkedFact = Expression.Parameter(typeof(TFact), "$temp");
var linkedKey = Expression.Constant($"$linkedkey{_linkedCount}$");

var action = Expression.Lambda<Action<IContext>>(
var insertContext = yieldInsert.Parameters[0];
var insertAction = Expression.Lambda<Action<IContext>>(
Expression.Block(
new[] {linkedFact},
Expression.Assign(linkedFact, Expression.Invoke(yieldInsert, insertContext)),
Expression.Call(insertContext,
typeof(IContext).GetTypeInfo().GetDeclaredMethod(nameof(IContext.InsertLinked)),
linkedKey, linkedFact)),
insertContext);

var updateContext = yieldUpdate.Parameters[0];
var updateAction = Expression.Lambda<Action<IContext>>(
Expression.Block(
new[] {linkedFact},
Expression.Assign(linkedFact,
Expression.Convert(
Expression.Call(context,
Expression.Call(updateContext,
typeof(IContext).GetTypeInfo().GetDeclaredMethod(nameof(IContext.GetLinked)),
linkedKey),
typeof(TFact))),
Expression.IfThenElse(
Expression.Equal(linkedFact, Expression.Constant(null)),
Expression.Call(context,
typeof(IContext).GetTypeInfo().GetDeclaredMethod(nameof(IContext.InsertLinked)), linkedKey,
yield.Body),
Expression.Call(context,
typeof(IContext).GetTypeInfo().GetDeclaredMethod(nameof(IContext.UpdateLinked)), linkedKey,
yield.Body))
),
context);
return Do(action);
}
Expression.Assign(linkedFact, Expression.Invoke(yieldUpdate, updateContext, linkedFact)),
Expression.Call(updateContext,
typeof(IContext).GetTypeInfo().GetDeclaredMethod(nameof(IContext.UpdateLinked)),
linkedKey, linkedFact)),
updateContext);

public IRightHandSideExpression Yield<TFact>(Expression<Func<IContext, TFact>> yieldInsert, Expression<Func<IContext, TFact, TFact>> yieldUpdate)
{
_linkedCount++;
var context = yieldInsert.Parameters[0];
var linkedFact = Expression.Parameter(typeof(TFact));
var linkedKey = Expression.Constant($"$linkedkey{_linkedCount}$");

var action = Expression.Lambda<Action<IContext>>(
var removeContext = yieldRemove.Parameters[0];
var removeAction = Expression.Lambda<Action<IContext>>(
Expression.Block(
new[] {linkedFact},
Expression.Assign(linkedFact,
Expression.Convert(
Expression.Call(context,
Expression.Call(removeContext,
typeof(IContext).GetTypeInfo().GetDeclaredMethod(nameof(IContext.GetLinked)),
linkedKey),
typeof(TFact))),
Expression.IfThenElse(
Expression.Equal(linkedFact, Expression.Constant(null)),
Expression.Call(context,
typeof(IContext).GetTypeInfo().GetDeclaredMethod(nameof(IContext.InsertLinked)), linkedKey,
yieldInsert.Body),
Expression.Block(
Expression.Assign(linkedFact, Expression.Invoke(yieldUpdate, context, linkedFact)),
Expression.Call(context,
typeof(IContext).GetTypeInfo().GetDeclaredMethod(nameof(IContext.UpdateLinked)),
linkedKey, linkedFact)))
Expression.Invoke(yieldRemove, removeContext, linkedFact),
Expression.Call(removeContext,
typeof(IContext).GetTypeInfo().GetDeclaredMethod(nameof(IContext.RetractLinked)),
linkedKey, linkedFact)
),
context);
return Do(action);
removeContext);

var rhs = Action(insertAction, ActionTrigger.Activated)
.Action(updateAction, ActionTrigger.Reactivated)
.Action(removeAction, ActionTrigger.Deactivated);
return rhs;
}
}
}
38 changes: 36 additions & 2 deletions src/NRules/NRules.RuleModel/ActionElement.cs
@@ -1,20 +1,54 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq.Expressions;

namespace NRules.RuleModel
{
/// <summary>
/// Activation events that trigger the actions.
/// </summary>
[Flags]
public enum ActionTrigger
{
/// <summary>
/// Action is not triggered.
/// </summary>
None = 0x0,

/// <summary>
/// Action is triggered when activation is created.
/// </summary>
Activated = 0x1,

/// <summary>
/// Action is triggered when activation is updated.
/// </summary>
Reactivated = 0x2,

/// <summary>
/// Action is triggered when activation is removed.
/// </summary>
Deactivated = 0x4,
}

/// <summary>
/// Action executed by the engine when the rule fires.
/// </summary>
[DebuggerDisplay("{Expression.ToString()}")]
public class ActionElement : ExpressionElement
{
internal ActionElement(IEnumerable<Declaration> declarations, IEnumerable<Declaration> references, LambdaExpression expression)
internal ActionElement(IEnumerable<Declaration> declarations, IEnumerable<Declaration> references, LambdaExpression expression, ActionTrigger actionTrigger)
: base(declarations, references, expression)
{
ActionTrigger = actionTrigger;
}

/// <summary>
/// Activation events that trigger this action.
/// </summary>
public ActionTrigger ActionTrigger { get; }

internal override void Accept<TContext>(TContext context, RuleElementVisitor<TContext> visitor)
{
visitor.VisitAction(context, this);
Expand Down
23 changes: 22 additions & 1 deletion src/NRules/NRules.RuleModel/Builders/ActionGroupBuilder.cs
Expand Up @@ -12,27 +12,48 @@ public class ActionGroupBuilder : RuleRightElementBuilder, IBuilder<ActionGroupE
{
private readonly List<ActionElement> _actions = new List<ActionElement>();

private const ActionTrigger DefaultTrigger = ActionTrigger.Activated | ActionTrigger.Reactivated;

internal ActionGroupBuilder(SymbolTable scope) : base(scope)
{
}

/// <summary>
/// Adds a rule action to the group.
/// The action will be executed on new and updated rule activations.
/// </summary>
/// <param name="expression">Rule action expression.
/// The first parameter of the action expression must be <see cref="IContext"/>.
/// Names and types of the rest of the expression parameters must match the names and types defined in the pattern declarations.</param>
public void Action(LambdaExpression expression)
{
Action(expression, DefaultTrigger);
}

/// <summary>
/// Adds a rule action to the group.
/// </summary>
/// <param name="expression">Rule action expression.
/// The first parameter of the action expression must be <see cref="IContext"/>.
/// Names and types of the rest of the expression parameters must match the names and types defined in the pattern declarations.</param>
/// <param name="actionTrigger">Activation events that trigger the action.</param>
public void Action(LambdaExpression expression, ActionTrigger actionTrigger)
{
if (expression.Parameters.Count == 0 ||
expression.Parameters.First().Type != typeof(IContext))
{
throw new ArgumentException(
$"Action expression must have {typeof(IContext)} as its first parameter");
}

if (actionTrigger == ActionTrigger.None)
{
throw new ArgumentException("Action trigger not specified");
}

IEnumerable<ParameterExpression> parameters = expression.Parameters.Skip(1);
IEnumerable<Declaration> references = parameters.Select(p => Scope.Lookup(p.Name, p.Type));
var actionElement = new ActionElement(Scope.VisibleDeclarations, references, expression);
var actionElement = new ActionElement(Scope.VisibleDeclarations, references, expression, actionTrigger);
_actions.Add(actionElement);
}

Expand Down
31 changes: 31 additions & 0 deletions src/NRules/NRules.RuleModel/IMatch.cs
Expand Up @@ -2,6 +2,32 @@

namespace NRules.RuleModel
{
/// <summary>
/// Event that triggered the match.
/// </summary>
public enum MatchTrigger
{
/// <summary>
/// Match is not active.
/// </summary>
None = 0,

/// <summary>
/// Match is triggered due to activation creation.
/// </summary>
Created = 1,

/// <summary>
/// Match is triggered due to activation update.
/// </summary>
Updated = 2,

/// <summary>
/// Match is triggered due to activation removal.
/// </summary>
Removed = 4,
}

/// <summary>
/// Represents a match of all rule's conditions.
/// </summary>
Expand All @@ -16,5 +42,10 @@ public interface IMatch
/// Facts matched by the rule.
/// </summary>
IEnumerable<IFactMatch> Facts { get; }

/// <summary>
/// Event that triggered the match.
/// </summary>
MatchTrigger Trigger { get; }
}
}
5 changes: 5 additions & 0 deletions src/NRules/NRules/ActionExecutor.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using NRules.RuleModel;
using NRules.Utilities;

namespace NRules
{
Expand Down Expand Up @@ -44,9 +46,12 @@ public void Execute(IExecutionContext executionContext, IActionContext actionCon
private IEnumerable<ActionInvocation> CreateInvocations(IExecutionContext executionContext, IActionContext actionContext)
{
ICompiledRule compiledRule = actionContext.CompiledRule;
MatchTrigger trigger = actionContext.Activation.Trigger;
var invocations = new List<ActionInvocation>();
foreach (IRuleAction action in compiledRule.Actions)
{
if (!trigger.Matches(action.Trigger)) continue;

var args = action.GetArguments(executionContext, actionContext);
var invocation = new ActionInvocation(executionContext, actionContext, action, args);
invocations.Add(invocation);
Expand Down

0 comments on commit 911d3ff

Please sign in to comment.