Entity Framework demonstration (#186)
* Initial Commit * WIP * Readability + null ef issue r&d * Cleanup * Minimum Sample * Refactored to remove EF from DemoApp * Fixed RuleValidator issue Added EmptyRulesTest for expected simple and nested rules empty exception Co-authored-by: Alex Reich <alex@alexreich.com> Co-authored-by: Alex Reich <Alex_Reich@mechanicsbank.com>pull/190/head
parent
b763f718bc
commit
9f898b703b
|
@ -26,6 +26,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RulesEngineBenchmark", "ben
|
||||||
{CD4DFE6A-083B-478E-8377-77F474833E30} = {CD4DFE6A-083B-478E-8377-77F474833E30}
|
{CD4DFE6A-083B-478E-8377-77F474833E30} = {CD4DFE6A-083B-478E-8377-77F474833E30}
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DemoApp.EFDataExample", "demo\DemoApp.EFDataExample\DemoApp.EFDataExample.csproj", "{E376D3E6-6890-4C09-9EA0-3EFD9C1E036D}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -48,6 +50,10 @@ Global
|
||||||
{C058809F-C720-4EFC-925D-A486627B238B}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
|
||||||
{C058809F-C720-4EFC-925D-A486627B238B}.Release|Any CPU.Build.0 = Release|Any CPU
|
{C058809F-C720-4EFC-925D-A486627B238B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E376D3E6-6890-4C09-9EA0-3EFD9C1E036D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{E376D3E6-6890-4C09-9EA0-3EFD9C1E036D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E376D3E6-6890-4C09-9EA0-3EFD9C1E036D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{E376D3E6-6890-4C09-9EA0-3EFD9C1E036D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
|
<RootNamespace>DemoApp.EFDataExample</RootNamespace>
|
||||||
|
<AssemblyName>DemoApp.EFDataExample</AssemblyName>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.8" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\RulesEngine\RulesEngine.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -0,0 +1,74 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using RulesEngine.Models;
|
||||||
|
|
||||||
|
namespace DemoApp.EFDataExample
|
||||||
|
{
|
||||||
|
public class RulesEngineDemoContext : DbContext
|
||||||
|
{
|
||||||
|
public DbSet<WorkflowRules> WorkflowRules { get; set; }
|
||||||
|
public DbSet<ActionInfo> ActionInfos { get; set; }
|
||||||
|
|
||||||
|
public DbSet<RuleActions> RuleActions { get; set; }
|
||||||
|
public DbSet<Rule> Rules { get; set; }
|
||||||
|
public DbSet<ScopedParam> ScopedParams { get; set; }
|
||||||
|
|
||||||
|
public string DbPath { get; private set; }
|
||||||
|
|
||||||
|
public RulesEngineDemoContext()
|
||||||
|
{
|
||||||
|
var folder = Environment.SpecialFolder.LocalApplicationData;
|
||||||
|
var path = Environment.GetFolderPath(folder);
|
||||||
|
DbPath = $"{path}{System.IO.Path.DirectorySeparatorChar}RulesEngineDemo.db";
|
||||||
|
}
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder options)
|
||||||
|
=> options.UseSqlite($"Data Source={DbPath}");
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity<ActionInfo>()
|
||||||
|
.Property(b => b.Context)
|
||||||
|
.HasConversion(
|
||||||
|
v => JsonConvert.SerializeObject(v),
|
||||||
|
v => JsonConvert.DeserializeObject<Dictionary<string, object>>(v));
|
||||||
|
|
||||||
|
modelBuilder.Entity<ActionInfo>()
|
||||||
|
.HasKey(k => k.Name);
|
||||||
|
|
||||||
|
modelBuilder.Entity<ScopedParam>()
|
||||||
|
.HasKey(k => k.Name);
|
||||||
|
|
||||||
|
modelBuilder.Entity<WorkflowRules>(entity => {
|
||||||
|
entity.HasKey(k => k.WorkflowName);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<RuleActions>(entity => {
|
||||||
|
entity.HasNoKey();
|
||||||
|
entity.HasOne(o => o.OnSuccess).WithMany();
|
||||||
|
entity.HasOne(o => o.OnFailure).WithMany();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<Rule>(entity => {
|
||||||
|
entity.HasKey(k => k.RuleName);
|
||||||
|
|
||||||
|
entity.Property(b => b.Properties)
|
||||||
|
.HasConversion(
|
||||||
|
v => JsonConvert.SerializeObject(v),
|
||||||
|
v => JsonConvert.DeserializeObject<Dictionary<string, object>>(v));
|
||||||
|
entity.Ignore(e => e.Actions);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<WorkflowRules>()
|
||||||
|
.Ignore(b => b.WorkflowRulesToInject);
|
||||||
|
|
||||||
|
modelBuilder.Entity<Rule>()
|
||||||
|
.Ignore(b => b.WorkflowRulesToInject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -9,6 +9,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<ProjectReference Include="../../src/RulesEngine/RulesEngine.csproj" />
|
<ProjectReference Include="../../src/RulesEngine/RulesEngine.csproj" />
|
||||||
|
<ProjectReference Include="..\DemoApp.EFDataExample\DemoApp.EFDataExample.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
|
||||||
|
using DemoApp.EFDataExample;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
using RulesEngine.Models;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Dynamic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using static RulesEngine.Extensions.ListofRuleResultTreeExtension;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace DemoApp
|
||||||
|
{
|
||||||
|
public class EFDemo
|
||||||
|
{
|
||||||
|
public void Run()
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Running {nameof(EFDemo)}....");
|
||||||
|
var basicInfo = "{\"name\": \"hello\",\"email\": \"abcy@xyz.com\",\"creditHistory\": \"good\",\"country\": \"canada\",\"loyalityFactor\": 3,\"totalPurchasesToDate\": 10000}";
|
||||||
|
var orderInfo = "{\"totalOrders\": 5,\"recurringItems\": 2}";
|
||||||
|
var telemetryInfo = "{\"noOfVisitsPerMonth\": 10,\"percentageOfBuyingToVisit\": 15}";
|
||||||
|
|
||||||
|
var converter = new ExpandoObjectConverter();
|
||||||
|
|
||||||
|
dynamic input1 = JsonConvert.DeserializeObject<ExpandoObject>(basicInfo, converter);
|
||||||
|
dynamic input2 = JsonConvert.DeserializeObject<ExpandoObject>(orderInfo, converter);
|
||||||
|
dynamic input3 = JsonConvert.DeserializeObject<ExpandoObject>(telemetryInfo, converter);
|
||||||
|
|
||||||
|
var inputs = new dynamic[]
|
||||||
|
{
|
||||||
|
input1,
|
||||||
|
input2,
|
||||||
|
input3
|
||||||
|
};
|
||||||
|
|
||||||
|
var files = Directory.GetFiles(Directory.GetCurrentDirectory(), "Discount.json", SearchOption.AllDirectories);
|
||||||
|
if (files == null || files.Length == 0)
|
||||||
|
throw new Exception("Rules not found.");
|
||||||
|
|
||||||
|
var fileData = File.ReadAllText(files[0]);
|
||||||
|
var workflowRules = JsonConvert.DeserializeObject<List<WorkflowRules>>(fileData);
|
||||||
|
|
||||||
|
RulesEngineDemoContext db = new RulesEngineDemoContext();
|
||||||
|
if (db.Database.EnsureCreated())
|
||||||
|
{
|
||||||
|
db.WorkflowRules.AddRange(workflowRules);
|
||||||
|
db.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
var wfr = db.WorkflowRules.Include(i => i.Rules).ThenInclude(i => i.Rules).ToArray();
|
||||||
|
|
||||||
|
var bre = new RulesEngine.RulesEngine(wfr, null);
|
||||||
|
|
||||||
|
string discountOffered = "No discount offered.";
|
||||||
|
|
||||||
|
List<RuleResultTree> resultList = bre.ExecuteAllRulesAsync("Discount", inputs).Result;
|
||||||
|
|
||||||
|
resultList.OnSuccess((eventName) => {
|
||||||
|
discountOffered = $"Discount offered is {eventName} % over MRP.";
|
||||||
|
});
|
||||||
|
|
||||||
|
resultList.OnFail(() => {
|
||||||
|
discountOffered = "The user is not eligible for any discount.";
|
||||||
|
});
|
||||||
|
|
||||||
|
Console.WriteLine(discountOffered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ namespace DemoApp
|
||||||
{
|
{
|
||||||
new BasicDemo().Run();
|
new BasicDemo().Run();
|
||||||
new NestedInputDemo().Run();
|
new NestedInputDemo().Run();
|
||||||
|
new EFDemo().Run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ namespace RulesEngine.Validators
|
||||||
{
|
{
|
||||||
When(c => c.Operator == null && c.RuleExpressionType == RuleExpressionType.LambdaExpression, () => {
|
When(c => c.Operator == null && c.RuleExpressionType == RuleExpressionType.LambdaExpression, () => {
|
||||||
RuleFor(c => c.Expression).NotEmpty().WithMessage(Constants.LAMBDA_EXPRESSION_EXPRESSION_NULL_ERRMSG);
|
RuleFor(c => c.Expression).NotEmpty().WithMessage(Constants.LAMBDA_EXPRESSION_EXPRESSION_NULL_ERRMSG);
|
||||||
RuleFor(c => c.Rules).Null().WithMessage(Constants.LAMBDA_EXPRESSION_RULES_ERRMSG);
|
RuleFor(c => c.Rules).Empty().WithMessage(Constants.LAMBDA_EXPRESSION_RULES_ERRMSG);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using RulesEngine.Models;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Dynamic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace RulesEngine.UnitTest
|
||||||
|
{
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public class EmptyRulesTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
private async Task EmptyRules_ReturnsExepectedResults()
|
||||||
|
{
|
||||||
|
var workflows = GetEmptyWorkflows();
|
||||||
|
var reSettings = new ReSettings { };
|
||||||
|
RulesEngine rulesEngine = new RulesEngine();
|
||||||
|
|
||||||
|
Func<Task> action = () => {
|
||||||
|
new RulesEngine(workflows, reSettings: reSettings);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
Exception ex = await Assert.ThrowsAsync<Exceptions.RuleValidationException>(action);
|
||||||
|
|
||||||
|
Assert.Contains("Atleast one of Rules or WorkflowRulesToInject must be not empty", ex.Message);
|
||||||
|
}
|
||||||
|
[Fact]
|
||||||
|
private async Task NestedRulesWithEmptyNestedActions_ReturnsExepectedResults()
|
||||||
|
{
|
||||||
|
var workflows = GetEmptyNestedWorkflows();
|
||||||
|
var reSettings = new ReSettings { };
|
||||||
|
RulesEngine rulesEngine = new RulesEngine();
|
||||||
|
|
||||||
|
Func<Task> action = () => {
|
||||||
|
new RulesEngine(workflows, reSettings: reSettings);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
Exception ex = await Assert.ThrowsAsync<Exceptions.RuleValidationException>(action);
|
||||||
|
|
||||||
|
Assert.Contains("Atleast one of Rules or WorkflowRulesToInject must be not empty", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorkflowRules[] GetEmptyWorkflows()
|
||||||
|
{
|
||||||
|
return new[] {
|
||||||
|
new WorkflowRules {
|
||||||
|
WorkflowName = "EmptyRulesTest",
|
||||||
|
Rules = new Rule[] {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorkflowRules[] GetEmptyNestedWorkflows()
|
||||||
|
{
|
||||||
|
return new[] {
|
||||||
|
new WorkflowRules {
|
||||||
|
WorkflowName = "EmptyNestedRulesTest",
|
||||||
|
Rules = new Rule[] {
|
||||||
|
new Rule {
|
||||||
|
RuleName = "AndRuleTrueFalse",
|
||||||
|
Operator = "And",
|
||||||
|
Rules = new Rule[] {
|
||||||
|
new Rule{
|
||||||
|
RuleName = "trueRule1",
|
||||||
|
Expression = "input1.TrueValue == true",
|
||||||
|
},
|
||||||
|
new Rule {
|
||||||
|
RuleName = "falseRule1",
|
||||||
|
Expression = "input1.TrueValue == false"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Rule {
|
||||||
|
RuleName = "OrRuleTrueFalse",
|
||||||
|
Operator = "Or",
|
||||||
|
Rules = new Rule[] {
|
||||||
|
new Rule{
|
||||||
|
RuleName = "trueRule2",
|
||||||
|
Expression = "input1.TrueValue == true",
|
||||||
|
},
|
||||||
|
new Rule {
|
||||||
|
RuleName = "falseRule2",
|
||||||
|
Expression = "input1.TrueValue == false"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Rule {
|
||||||
|
RuleName = "AndRuleFalseTrue",
|
||||||
|
Operator = "And",
|
||||||
|
Rules = new Rule[] {
|
||||||
|
new Rule{
|
||||||
|
RuleName = "trueRule3",
|
||||||
|
Expression = "input1.TrueValue == false",
|
||||||
|
},
|
||||||
|
new Rule {
|
||||||
|
RuleName = "falseRule4",
|
||||||
|
Expression = "input1.TrueValue == true"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Rule {
|
||||||
|
RuleName = "OrRuleFalseTrue",
|
||||||
|
Operator = "Or",
|
||||||
|
Rules = new Rule[] {
|
||||||
|
new Rule{
|
||||||
|
RuleName = "trueRule3",
|
||||||
|
Expression = "input1.TrueValue == false",
|
||||||
|
},
|
||||||
|
new Rule {
|
||||||
|
RuleName = "falseRule4",
|
||||||
|
Expression = "input1.TrueValue == true"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new WorkflowRules {
|
||||||
|
WorkflowName = "EmptyNestedRulesActionsTest",
|
||||||
|
Rules = new Rule[] {
|
||||||
|
new Rule {
|
||||||
|
RuleName = "AndRuleTrueFalse",
|
||||||
|
Operator = "And",
|
||||||
|
Rules = new Rule[] {
|
||||||
|
|
||||||
|
},
|
||||||
|
Actions = new RuleActions {
|
||||||
|
OnFailure = new ActionInfo{
|
||||||
|
Name = "OutputExpression",
|
||||||
|
Context = new Dictionary<string, object> {
|
||||||
|
{ "Expression", "input1.TrueValue" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue