zerocodebreengine / RuleExecutionService.cs
zombee11's picture
Upload 39 files
af8cc55 verified
Raw
History Blame Contribute Delete
9.49 kB
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using OptimAI.BRE.RuleEngine.Domain;
using OptimAI.BRE.Shared.Domain;
namespace OptimAI.BRE.RuleEngine.Application;
public sealed class RuleExecutionService : IRuleExecutionService
{
private readonly IRuleLoader _ruleLoader;
private readonly IConditionEvaluator _conditionEvaluator;
private readonly IActionExecutor _actionExecutor;
private readonly IRiskScoringEngine _riskScoringEngine;
private readonly ILogger<RuleExecutionService> _logger;
public RuleExecutionService(
IRuleLoader ruleLoader,
IConditionEvaluator conditionEvaluator,
IActionExecutor actionExecutor,
IRiskScoringEngine riskScoringEngine,
ILogger<RuleExecutionService> logger)
{
_ruleLoader = ruleLoader;
_conditionEvaluator = conditionEvaluator;
_actionExecutor = actionExecutor;
_riskScoringEngine = riskScoringEngine;
_logger = logger;
}
public async Task<ExecutionResult> ExecuteAsync(ExecutionContext context, CancellationToken ct = default)
{
var sw = Stopwatch.StartNew();
var state = new ExecutionState();
var ruleResults = new List<RuleEvaluationResult>();
try
{
var rules = await _ruleLoader.LoadRulesAsync(new RuleLoadRequest
{
TenantId = context.TenantId,
ProductCode = context.ProductCode,
BranchCode = context.BranchCode,
StageCode = context.StageCode,
RuleSetId = context.RuleSetId,
PublishedOnly = true
}, ct);
_logger.LogInformation("Executing {Count} rules for tenant {TenantId}, correlation {CorrelationId}",
rules.Count, context.TenantId, context.CorrelationId);
var executionState = new RuleExecutionState
{
GlobalState = state,
Data = context.Data,
TenantId = context.TenantId
};
var orderedRules = rules.OrderBy(r => r.Priority).ToList();
int executionOrder = 0;
foreach (var rule in orderedRules)
{
if (ct.IsCancellationRequested) break;
if (state.ShouldStop) break;
var ruleResult = await EvaluateRuleAsync(rule, context, ct);
ruleResult.ExecutionOrder = ++executionOrder;
ruleResults.Add(ruleResult);
if (ruleResult.IsMatched && !ruleResult.HasError)
{
foreach (var action in rule.CurrentVersion!.RuleDefinition.Actions)
{
await _actionExecutor.ExecuteAsync(action, context, executionState);
}
if (context.Options.StopOnFirstReject && state.CurrentDecision == Decision.Reject)
{
state.ShouldStop = true;
}
}
}
// Calculate risk score
var riskResult = await _riskScoringEngine.CalculateAsync(context, ruleResults);
state.RiskScore = riskResult.Score;
// Determine final traffic light
state.TrafficLight = DetermineTrafficLight(state.CurrentDecision, riskResult.Category);
sw.Stop();
return BuildResult(context, state, ruleResults, riskResult, sw.ElapsedMilliseconds);
}
catch (Exception ex)
{
_logger.LogError(ex, "Rule execution failed for tenant {TenantId}", context.TenantId);
throw;
}
}
public async Task<RuleEvaluationResult> EvaluateRuleAsync(Rule rule, ExecutionContext context, CancellationToken ct = default)
{
var sw = Stopwatch.StartNew();
var result = new RuleEvaluationResult
{
RuleId = rule.Id,
RuleCode = rule.RuleCode,
RuleName = rule.RuleName,
VersionNumber = rule.CurrentVersion?.VersionNumber ?? 0
};
try
{
if (rule.CurrentVersion?.RuleDefinition == null)
{
result.HasError = true;
result.ErrorMessage = "Rule has no published version";
return result;
}
var definition = rule.CurrentVersion.RuleDefinition;
// Evaluate conditions with detailed tracking
var (matched, conditionResults) = await EvaluateConditionGroupDetailedAsync(
definition.Conditions, context.Data);
result.IsMatched = matched;
result.ConditionResults = conditionResults;
}
catch (Exception ex)
{
result.HasError = true;
result.ErrorMessage = ex.Message;
_logger.LogWarning(ex, "Error evaluating rule {RuleCode}", rule.RuleCode);
}
finally
{
sw.Stop();
result.ExecutionMs = (int)sw.ElapsedMilliseconds;
}
return result;
}
public async Task<bool> EvaluateConditionGroupAsync(ConditionGroup group, DynamicDataContext data, CancellationToken ct = default)
{
var results = new List<bool>();
foreach (var node in group.Rules)
{
bool nodeResult;
if (node.IsGroup && node.Group != null)
nodeResult = await EvaluateConditionGroupAsync(node.Group, data, ct);
else
nodeResult = _conditionEvaluator.Evaluate(node, data);
results.Add(nodeResult);
}
return group.Operator switch
{
LogicalOperator.And => results.All(r => r),
LogicalOperator.Or => results.Any(r => r),
LogicalOperator.Not => results.Count > 0 && !results[0],
_ => false
};
}
private async Task<(bool matched, List<ConditionEvaluationResult> details)> EvaluateConditionGroupDetailedAsync(
ConditionGroup group, DynamicDataContext data)
{
var details = new List<ConditionEvaluationResult>();
var results = new List<bool>();
foreach (var node in group.Rules)
{
bool nodeResult;
if (node.IsGroup && node.Group != null)
{
var (groupResult, groupDetails) = await EvaluateConditionGroupDetailedAsync(node.Group, data);
nodeResult = groupResult;
details.AddRange(groupDetails);
}
else
{
var actualValue = data.GetValue(node.Field!);
nodeResult = _conditionEvaluator.Evaluate(node, data);
details.Add(new ConditionEvaluationResult
{
ConditionId = node.Id,
Field = node.Field ?? "",
Operator = node.Operator?.ToString() ?? "",
ExpectedValue = node.Value,
ActualValue = actualValue,
Result = nodeResult
});
}
results.Add(nodeResult);
}
bool finalResult = group.Operator switch
{
LogicalOperator.And => results.All(r => r),
LogicalOperator.Or => results.Any(r => r),
LogicalOperator.Not => results.Count > 0 && !results[0],
_ => false
};
return (finalResult, details);
}
private static TrafficLight DetermineTrafficLight(Decision decision, RiskCategory riskCategory)
{
return decision switch
{
Decision.Approve when riskCategory is RiskCategory.Low or RiskCategory.Medium => TrafficLight.Green,
Decision.Reject => TrafficLight.Red,
Decision.Deviation => TrafficLight.Amber,
_ => riskCategory switch
{
RiskCategory.Critical => TrafficLight.Red,
RiskCategory.High => TrafficLight.Amber,
_ => TrafficLight.Green
}
};
}
private static ExecutionResult BuildResult(
ExecutionContext context,
ExecutionState state,
List<RuleEvaluationResult> ruleResults,
RiskScoreResult riskResult,
long elapsedMs)
{
return new ExecutionResult
{
TenantId = context.TenantId,
FinalDecision = state.CurrentDecision,
RiskScore = state.RiskScore,
RiskCategory = riskResult.Category,
TrafficLight = state.TrafficLight,
TotalRulesEvaluated = ruleResults.Count,
RulesPassed = ruleResults.Count(r => r.IsMatched),
RulesFailed = ruleResults.Count(r => !r.IsMatched),
RulesSkipped = ruleResults.Count(r => r.HasError),
DeviationsCount = state.Deviations.Count,
ExecutionMs = (int)elapsedMs,
RuleResults = ruleResults.Select(r => new RuleExecutionSummary
{
RuleId = r.RuleId,
RuleCode = r.RuleCode,
RuleName = r.RuleName,
VersionNumber = r.VersionNumber,
IsMatched = r.IsMatched,
ConditionsEvaluated = r.ConditionResults,
ActionsExecuted = r.ExecutedActions,
ExecutionMs = r.ExecutionMs,
ErrorMessage = r.ErrorMessage
}).ToList(),
FieldValues = state.OutputFields
};
}
}