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
Alex Reich 2021-07-22 09:29:26 -07:00 committed by GitHub
parent b763f718bc
commit 9f898b703b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 330 additions and 1 deletions

View File

@ -26,6 +26,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RulesEngineBenchmark", "ben
{CD4DFE6A-083B-478E-8377-77F474833E30} = {CD4DFE6A-083B-478E-8377-77F474833E30}
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DemoApp.EFDataExample", "demo\DemoApp.EFDataExample\DemoApp.EFDataExample.csproj", "{E376D3E6-6890-4C09-9EA0-3EFD9C1E036D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<ProjectReference Include="../../src/RulesEngine/RulesEngine.csproj" />
<ProjectReference Include="..\DemoApp.EFDataExample\DemoApp.EFDataExample.csproj" />
</ItemGroup>
<ItemGroup>

73
demo/DemoApp/EFDemo.cs Normal file
View File

@ -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);
}
}
}

View File

@ -9,6 +9,7 @@ namespace DemoApp
{
new BasicDemo().Run();
new NestedInputDemo().Run();
new EFDemo().Run();
}
}
}

View File

@ -39,7 +39,7 @@ namespace RulesEngine.Validators
{
When(c => c.Operator == null && c.RuleExpressionType == RuleExpressionType.LambdaExpression, () => {
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);
});
}

View File

@ -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" }
}
}
}
}
}
}
};
}
}
}