perf improvements and benchmarking (#70)

* added perf improvements

* Added benchmark tool
pull/75/head v3.0.0-preview.2
Abbas Cyclewala 2020-11-16 14:06:35 +05:30 committed by GitHub
parent 7b05b0a656
commit a90880f126
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 349 additions and 86 deletions

View File

@ -1,10 +1,34 @@
# CHANGELOG
All notable changes to this project will be documented in this file.
## [3.0.0-preview.2]
- Made LocalParams and ErrorMessage formatting optional via ReSettings
- Major performance improvement
- 25% improvement from previous version
- Upto 35% improvement by disabling optional features
## [3.0.0-preview.1] - 23-10-2020
- Renamed `ExecuteRule` to `ExecuteAllRulesAsync`
- Added Actions support. More details on [actions wiki](https://github.com/microsoft/RulesEngine/wiki/Actions)
## [2.1.5] - 02-11-2020
- Added `Properties` field to Rule to allow custom fields to Rule
## [2.1.4] - 15-10-2020
- Added exception data properties to identify RuleName.
## [2.1.3] - 12-10-2020
- Optional parameter for rethrow exception on failure of expression compilation.
## [2.1.2] - 02-10-2020
- Fixed binary expression requirement. Now any expression will work as long as it evalutes to boolean.
## [2.1.1] - 01-09-2020
- Fixed exception thrown when errormessage field is null
- Added better messaging when identifier is not found in expression
- Fixed other minor bugs
## [2.1.0] - 18-05-2020
- Adding local param support to make expression authroing more intuitive.

View File

@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29123.89
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RulesEngine", "src\RulesEngine\\RulesEngine.csproj", "{CD4DFE6A-083B-478E-8377-77F474833E30}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RulesEngine", "src\RulesEngine\RulesEngine.csproj", "{CD4DFE6A-083B-478E-8377-77F474833E30}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RulesEngine.UnitTest", "test\RulesEngine.UnitTest\RulesEngine.UnitTest.csproj", "{50E0C2A5-E2C8-4B12-8C0E-B69F698A82BF}"
EndProject
@ -17,6 +17,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
global.json = global.json
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RulesEngineBenchmark", "benchmark\RulesEngineBenchmark\RulesEngineBenchmark.csproj", "{C058809F-C720-4EFC-925D-A486627B238B}"
ProjectSection(ProjectDependencies) = postProject
{CD4DFE6A-083B-478E-8377-77F474833E30} = {CD4DFE6A-083B-478E-8377-77F474833E30}
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -35,6 +40,10 @@ Global
{57BB8C07-799A-4F87-A7CC-D3D3F694DD02}.Debug|Any CPU.Build.0 = Debug|Any CPU
{57BB8C07-799A-4F87-A7CC-D3D3F694DD02}.Release|Any CPU.ActiveCfg = Release|Any CPU
{57BB8C07-799A-4F87-A7CC-D3D3F694DD02}.Release|Any CPU.Build.0 = Release|Any CPU
{C058809F-C720-4EFC-925D-A486627B238B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C058809F-C720-4EFC-925D-A486627B238B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C058809F-C720-4EFC-925D-A486627B238B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C058809F-C720-4EFC-925D-A486627B238B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -0,0 +1,81 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Newtonsoft.Json;
using RulesEngine.Models;
using System;
using System.Collections.Generic;
using System.IO;
namespace RulesEngineBenchmark
{
[MemoryDiagnoser]
public class REBenchmark
{
private readonly RulesEngine.RulesEngine rulesEngine;
private readonly object ruleInput;
private readonly List<WorkflowRules> workflows;
class ListItem
{
public int Id { get; set; }
public string Value { get; set; }
}
public REBenchmark()
{
var files = Directory.GetFiles(Directory.GetCurrentDirectory(), "NestedInputDemo.json", SearchOption.AllDirectories);
if (files == null || files.Length == 0)
throw new Exception("Rules not found.");
var fileData = File.ReadAllText(files[0]);
workflows = JsonConvert.DeserializeObject<List<WorkflowRules>>(fileData);
rulesEngine = new RulesEngine.RulesEngine(workflows.ToArray(), null,new ReSettings {
EnableFormattedErrorMessage = false,
EnableLocalParams = false
});
ruleInput = new
{
SimpleProp = "simpleProp",
NestedProp = new
{
SimpleProp = "nestedSimpleProp",
ListProp = new List<ListItem>
{
new ListItem
{
Id = 1,
Value = "first"
},
new ListItem
{
Id = 2,
Value = "second"
}
}
}
};
}
[Params(1000, 10000)]
public int N;
[Benchmark]
public void RuleExecutionDefault()
{
foreach (var workflow in workflows)
{
List<RuleResultTree> resultList = rulesEngine.ExecuteAllRulesAsync(workflow.WorkflowName, ruleInput).Result;
}
}
}
class Program
{
static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<REBenchmark>();
}
}
}

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\RulesEngine\RulesEngine.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Workflows\Discount.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Workflows\NestedInputDemo.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,92 @@
[
{
"WorkflowName": "Discount",
"Rules": [
{
"RuleName": "GiveDiscount10",
"SuccessEvent": "10",
"ErrorMessage": "One or more adjust rules failed.",
"ErrorType": "Error",
"RuleExpressionType": "LambdaExpression",
"Expression": "input1.country == \"india\" AND input1.loyalityFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2"
},
{
"RuleName": "GiveDiscount20",
"SuccessEvent": "20",
"ErrorMessage": "One or more adjust rules failed.",
"ErrorType": "Error",
"RuleExpressionType": "LambdaExpression",
"Expression": "input1.country == \"india\" AND input1.loyalityFactor == 3 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2"
},
{
"RuleName": "GiveDiscount25",
"SuccessEvent": "25",
"ErrorMessage": "One or more adjust rules failed.",
"ErrorType": "Error",
"RuleExpressionType": "LambdaExpression",
"Expression": "input1.country != \"india\" AND input1.loyalityFactor >= 2 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 5"
},
{
"RuleName": "GiveDiscount30",
"SuccessEvent": "30",
"ErrorMessage": "One or more adjust rules failed.",
"ErrorType": "Error",
"RuleExpressionType": "LambdaExpression",
"Expression": "input1.loyalityFactor > 3 AND input1.totalPurchasesToDate >= 50000 AND input1.totalPurchasesToDate <= 100000 AND input2.totalOrders > 5 AND input3.noOfVisitsPerMonth > 15"
},
{
"RuleName": "GiveDiscount30NestedOrExample",
"SuccessEvent": "30",
"ErrorMessage": "One or more adjust rules failed.",
"ErrorType": "Error",
"Operator": "OrElse",
"Rules":[
{
"RuleName": "IsLoyalAndHasGoodSpend",
"ErrorMessage": "One or more adjust rules failed.",
"ErrorType": "Error",
"RuleExpressionType": "LambdaExpression",
"Expression": "input1.loyalityFactor > 3 AND input1.totalPurchasesToDate >= 50000 AND input1.totalPurchasesToDate <= 100000"
},
{
"RuleName": "OrHasHighNumberOfTotalOrders",
"ErrorMessage": "One or more adjust rules failed.",
"ErrorType": "Error",
"RuleExpressionType": "LambdaExpression",
"Expression": "input2.totalOrders > 15"
}
]
},
{
"RuleName": "GiveDiscount35NestedAndExample",
"SuccessEvent": "35",
"ErrorMessage": "One or more adjust rules failed.",
"ErrorType": "Error",
"Operator": "AndAlso",
"Rules": [
{
"RuleName": "IsLoyal",
"ErrorMessage": "One or more adjust rules failed.",
"ErrorType": "Error",
"RuleExpressionType": "LambdaExpression",
"Expression": "input1.loyalityFactor > 3"
},
{
"RuleName": "AndHasTotalPurchased100000",
"ErrorMessage": "One or more adjust rules failed.",
"ErrorType": "Error",
"RuleExpressionType": "LambdaExpression",
"Expression": "input1.totalPurchasesToDate >= 100000"
},
{
"RuleName": "AndOtherConditions",
"ErrorMessage": "One or more adjust rules failed.",
"ErrorType": "Error",
"RuleExpressionType": "LambdaExpression",
"Expression": "input2.totalOrders > 15 AND input3.noOfVisitsPerMonth > 25"
}
]
}
]
}
]

View File

@ -0,0 +1,39 @@
[
{
"WorkflowName": "NestedInputDemoWorkflow1",
"Rules": [
{
"RuleName": "CheckNestedSimpleProp",
"ErrorMessage": "One or more adjust rules failed.",
"ErrorType": "Error",
"RuleExpressionType": "LambdaExpression",
"Expression": "input1.NestedProp.SimpleProp == \"nestedSimpleProp\""
}
]
},
{
"WorkflowName": "NestedInputDemoWorkflow2",
"Rules": [
{
"RuleName": "CheckNestedListProp",
"ErrorMessage": "One or more adjust rules failed.",
"ErrorType": "Error",
"RuleExpressionType": "LambdaExpression",
"Expression": "input1.NestedProp.ListProp[0].Id == 1 && input1.NestedProp.ListProp[1].Value == \"second\""
}
]
},
{
"WorkflowName": "NestedInputDemoWorkflow3",
"Rules": [
{
"RuleName": "CheckNestedListPropFunctions",
"ErrorMessage": "One or more adjust rules failed.",
"ErrorType": "Error",
"RuleExpressionType": "LambdaExpression",
"Expression": "input1.NestedProp.ListProp[1].Value.ToUpper() = \"SECOND\""
}
]
}
]

View File

@ -17,7 +17,7 @@ namespace RulesEngine.Actions
public override ValueTask<object> Run(ActionContext context, RuleParameter[] ruleParameters)
{
var expression = context.GetContext<string>("expression");
return new ValueTask<object>(_ruleExpressionParser.Evaluate(expression, ruleParameters));
return new ValueTask<object>(_ruleExpressionParser.Evaluate<object>(expression, ruleParameters));
}
}
}

View File

@ -25,8 +25,8 @@ namespace RulesEngine.ExpressionBuilders
{
try
{
var ruleDelegate = _ruleExpressionParser.Compile(rule.Expression, ruleParams,typeof(bool));
bool func(object[] paramList) => (bool)ruleDelegate.DynamicInvoke(paramList);
var ruleDelegate = _ruleExpressionParser.Compile<bool>(rule.Expression, ruleParams);
bool func(object[] paramList) => ruleDelegate(paramList);
return Helpers.ToResultTree(rule, null, func);
}
catch (Exception ex)

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Linq.Expressions;
using FastExpressionCompiler;
namespace RulesEngine.ExpressionBuilders
{
@ -21,23 +22,34 @@ namespace RulesEngine.ExpressionBuilders
});
}
public Delegate Compile(string expression, RuleParameter[] ruleParams, Type returnType = null)
public Func<object[],T> Compile<T>(string expression, RuleParameter[] ruleParams)
{
var cacheKey = GetCacheKey(expression,ruleParams,returnType);
var cacheKey = GetCacheKey(expression,ruleParams,typeof(T));
return _memoryCache.GetOrCreate(cacheKey,(entry) => {
entry.SetSize(1);
var config = new ParsingConfig { CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes) };
var typeParamExpressions = GetParameterExpression(ruleParams).ToArray();
var e = DynamicExpressionParser.ParseLambda(config, true, typeParamExpressions.ToArray(), returnType, expression);
return e.Compile();
var e = DynamicExpressionParser.ParseLambda(config, true, typeParamExpressions.ToArray(), typeof(T), expression);
var wrappedExpression = WrapExpression<T>(e,typeParamExpressions);
return wrappedExpression.CompileFast<Func<object[],T>>();
});
}
public object Evaluate(string expression, RuleParameter[] ruleParams, Type returnType = null)
private Expression<Func<object[],T>> WrapExpression<T>(Expression expression, ParameterExpression[] parameters){
var argExp = Expression.Parameter(typeof(object[]),"args");
var paramExps = parameters.Select((c,i) => {
var arg = Expression.ArrayAccess(argExp,Expression.Constant(i));
return Expression.Convert(arg,c.Type);
});
var invokeExp = Expression.Invoke(expression,paramExps);
return Expression.Lambda<Func<object[],T>>(invokeExp, argExp);
}
public T Evaluate<T>(string expression, RuleParameter[] ruleParams)
{
var func = Compile(expression, ruleParams, returnType);
return func.DynamicInvoke(ruleParams.Select(c => c.Value).ToArray());
var func = Compile<T>(expression, ruleParams);
return func(ruleParams.Select(c => c.Value).ToArray());
}
// <summary>

View File

@ -11,33 +11,12 @@ namespace RulesEngine.Models
[ExcludeFromCodeCoverage]
internal class CompiledParam
{
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>
/// The name.
/// </value>
internal string Name { get; set; }
/// <summary>
/// Gets or sets the value.
/// </summary>
/// <value>
/// The value.
/// </value>
internal Delegate Value { get; set; }
/// <summary>
/// Gets or sets the parameters.
/// </summary>
/// <value>
/// The parameters.
/// </value>
internal IEnumerable<RuleParameter> Parameters { get; set; }
internal Type ReturnType { get; set; }
internal Func<object[],object> Value { get; set; }
internal RuleParameter AsRuleParameter()
{
return new RuleParameter(Name,Value.Method.ReturnType);
return new RuleParameter(Name,ReturnType);
}
}
}

View File

@ -12,9 +12,9 @@ namespace RulesEngine.Models
public class ReSettings
{
public Type[] CustomTypes { get; set; }
public Dictionary<string, Func<ActionBase>> CustomActions { get; set; }
public bool EnableExceptionAsErrorMessage { get; set; } = true;
public bool EnableFormattedErrorMessage {get; set; } = true;
public bool EnableLocalParams {get;set;} = true;
}
}

View File

@ -17,13 +17,12 @@ namespace RulesEngine.Models
public class Rule
{
public string RuleName { get; set; }
/// <summary>
/// Gets or sets the custom property or tags of the rule.
/// </summary>
/// <value>
/// The properties of the rule.
/// </value>
/// <summary>
/// Gets or sets the custom property or tags of the rule.
/// </summary>
/// <value>
/// The properties of the rule.
/// </value>
public Dictionary<string, object> Properties { get; set; }
public string Operator { get; set; }
public string ErrorMessage { get; set; }

View File

@ -1,11 +1,9 @@
using Microsoft.Extensions.Logging;
using RulesEngine.Models;
using RulesEngine.Models;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
using System.Linq;
using RulesEngine.ExpressionBuilders;
using RulesEngine.HelperFunctions;
namespace RulesEngine
{
@ -41,9 +39,9 @@ namespace RulesEngine
var evaluatedParameters = new List<RuleParameter>();
foreach (var param in rule.LocalParams)
{
var compiledParam = GetDelegateForRuleParam(param, ruleParams.ToArray());
compiledParameters.Add(new CompiledParam { Name = param.Name, Value = compiledParam, Parameters = evaluatedParameters });
var evaluatedParam = EvaluateCompiledParam(param.Name, compiledParam, ruleParams);
var compiledParamDelegate = GetDelegateForRuleParam(param, ruleParams.ToArray());
var evaluatedParam = EvaluateCompiledParam(param.Name, compiledParamDelegate, ruleParams);
compiledParameters.Add(new CompiledParam { Name = param.Name, Value = compiledParamDelegate, ReturnType = evaluatedParam.Type });
ruleParams = ruleParams.Append(evaluatedParam);
evaluatedParameters.Add(evaluatedParam);
}
@ -56,9 +54,9 @@ namespace RulesEngine
/// <param name="compiledParam">The compiled parameter.</param>
/// <param name="ruleParams">The rule parameters.</param>
/// <returns>RuleParameter.</returns>
public RuleParameter EvaluateCompiledParam(string paramName, Delegate compiledParam, IEnumerable<RuleParameter> inputs)
public RuleParameter EvaluateCompiledParam(string paramName, Func<object[],object> compiledParam, IEnumerable<RuleParameter> inputs)
{
var result = compiledParam.DynamicInvoke(inputs.Select(c => c.Value).ToArray());
var result = compiledParam(inputs.Select(c => c.Value).ToArray());
return new RuleParameter(paramName, result);
}
@ -70,9 +68,9 @@ namespace RulesEngine
/// <param name="typeParameterExpressions">The type parameter expressions.</param>
/// <param name="ruleInputExp">The rule input exp.</param>
/// <returns></returns>
private Delegate GetDelegateForRuleParam(LocalParam param, RuleParameter[] ruleParameters)
private Func<object[],object> GetDelegateForRuleParam(LocalParam param, RuleParameter[] ruleParameters)
{
return _ruleExpressionParser.Compile(param.Expression, ruleParameters);
return _ruleExpressionParser.Compile<object>(param.Expression, ruleParameters);
}
}
}

View File

@ -9,8 +9,8 @@ namespace RulesEngine
{
internal class RuleExpressionBuilderFactory
{
private ReSettings _reSettings;
private LambdaExpressionBuilder _lambdaExpressionBuilder;
private readonly ReSettings _reSettings;
private readonly LambdaExpressionBuilder _lambdaExpressionBuilder;
public RuleExpressionBuilderFactory(ReSettings reSettings, RuleExpressionParser expressionParser)
{
_reSettings = reSettings;

View File

@ -1,7 +1,7 @@
using System.Threading.Tasks;
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Threading.Tasks;
using FluentValidation;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
@ -234,7 +234,7 @@ namespace RulesEngine
if (workflowRules != null)
{
var dictFunc = new Dictionary<string,RuleFunc<RuleResultTree>>();
foreach (var rule in _rulesCache.GetRules(workflowName))
foreach (var rule in workflowRules.Rules)
{
dictFunc.Add(rule.RuleName,CompileRule(workflowName, ruleParams, rule));
}
@ -261,6 +261,9 @@ namespace RulesEngine
private RuleFunc<RuleResultTree> CompileRule(string workflowName, RuleParameter[] ruleParams, Rule rule)
{
if(!_reSettings.EnableLocalParams){
return _ruleCompiler.CompileRule(rule,ruleParams);
}
var compiledParamsKey = GetCompiledParamsCacheKey(workflowName, rule.RuleName, ruleParams);
IEnumerable<CompiledParam> compiledParamList = _compiledParamsCache.GetOrCreate(compiledParamsKey, (entry) => _ruleParamCompiler.CompileParamsExpression(rule, ruleParams));
var compiledRuleParameters = compiledParamList?.Select(c => c.AsRuleParameter()) ?? new List<RuleParameter>();
@ -335,35 +338,36 @@ namespace RulesEngine
/// <returns>Updated error message.</returns>
private IEnumerable<RuleResultTree> FormatErrorMessages(IEnumerable<RuleResultTree> ruleResultList)
{
if(_reSettings.EnableFormattedErrorMessage){
foreach (var ruleResult in ruleResultList?.Where(r => !r.IsSuccess))
{
var errorMessage = ruleResult?.Rule?.ErrorMessage;
if(errorMessage != null){
var errorParameters = Regex.Matches(errorMessage, ParamParseRegex);
foreach (var ruleResult in ruleResultList?.Where(r => !r.IsSuccess))
{
var errorMessage = ruleResult?.Rule?.ErrorMessage;
if(errorMessage != null){
var errorParameters = Regex.Matches(errorMessage, ParamParseRegex);
var inputs = ruleResult.Inputs;
foreach (var param in errorParameters)
{
var paramVal = param?.ToString();
var property = paramVal?.Substring(2, paramVal.Length - 3);
if (property?.Split('.')?.Count() > 1)
var inputs = ruleResult.Inputs;
foreach (var param in errorParameters)
{
var typeName = property?.Split('.')?[0];
var propertyName = property?.Split('.')?[1];
errorMessage = UpdateErrorMessage(errorMessage, inputs, property, typeName, propertyName);
}
else
{
var arrParams = inputs?.Select(c => new { Name = c.Key, c.Value });
var model = arrParams?.Where(a => string.Equals(a.Name, property))?.FirstOrDefault();
var value = model?.Value != null ? JsonConvert.SerializeObject(model?.Value) : null;
errorMessage = errorMessage?.Replace($"$({property})", value ?? $"$({property})");
var paramVal = param?.ToString();
var property = paramVal?.Substring(2, paramVal.Length - 3);
if (property?.Split('.')?.Count() > 1)
{
var typeName = property?.Split('.')?[0];
var propertyName = property?.Split('.')?[1];
errorMessage = UpdateErrorMessage(errorMessage, inputs, property, typeName, propertyName);
}
else
{
var arrParams = inputs?.Select(c => new { Name = c.Key, c.Value });
var model = arrParams?.Where(a => string.Equals(a.Name, property))?.FirstOrDefault();
var value = model?.Value != null ? JsonConvert.SerializeObject(model?.Value) : null;
errorMessage = errorMessage?.Replace($"$({property})", value ?? $"$({property})");
}
}
ruleResult.ExceptionMessage = errorMessage;
}
ruleResult.ExceptionMessage = errorMessage;
}
}
return ruleResultList;
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Version>3.0.0-preview.1</Version>
<Version>3.0.0-preview.2</Version>
<Copyright>Copyright (c) Microsoft Corporation.</Copyright>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://github.com/microsoft/RulesEngine</PackageProjectUrl>
@ -25,6 +25,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FastExpressionCompiler" Version="2.0.0" />
<PackageReference Include="FluentValidation" Version="9.0.1" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.6" />

View File

@ -242,7 +242,7 @@ namespace RulesEngine.UnitTest
var fileData = File.ReadAllText(files[0]);
var bre = new RulesEngine(JsonConvert.DeserializeObject<WorkflowRules[]>(fileData), null);
var result = await bre.ExecuteAllRulesAsync("inputWorkflow", ruleParams?.ToArray()); ;
var result = await bre.ExecuteAllRulesAsync("inputWorkflow", ruleParams?.ToArray());
var ruleResult = result?.FirstOrDefault(r => string.Equals(r.Rule.RuleName, "GiveDiscount10", StringComparison.OrdinalIgnoreCase));
Assert.True(ruleResult.IsSuccess);
}