-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add streamlined way for building conditions #143
Conversation
@luispfgarces can you please let me know if you agree with the modifications before changing all the tests and samples to use this streamlined way? |
samples/Rules.Framework.WebUI.Sample/ReadmeExample/BasicRulesEngineExample.cs
Outdated
Show resolved
Hide resolved
Hi @jdromeiro, I like your approach, but if you'll allow me, let me suggest taking this even further. I have created a small graph representing the calls to create a rule's conditions. Basically, it only changes the way we start defining conditions (the root condition) and the composed nodes (and/or), the rest is just naming. Let's get one example:
RuleBuilder.NewRule<ContentTypes, ConditionTypes>()
.WithName("Benchmark 2 - Bohemian Rapsody")
.WithDateBegin(DateTime.Parse("2000-01-01"))
.WithContent(ContentTypes.Songs, "Bohemian Rapsody")
.WithCondition(x =>
{
return x.AsComposed()
.WithLogicalOperator(LogicalOperators.And)
.AddCondition(c =>
c.AsValued(ConditionTypes.Artist)
.OfDataType<string>()
.WithComparisonOperator(Operators.Equal)
.SetOperand("Queen")
.Build())
.AddCondition(c =>
c.AsValued(ConditionTypes.Lyrics)
.OfDataType<string>()
.WithComparisonOperator(Operators.Contains)
.SetOperand("real life")
.Build())
.AddCondition(c =>
c.AsValued(ConditionTypes.ReleaseYear)
.OfDataType<int>()
.WithComparisonOperator(Operators.GreaterThanOrEqual)
.SetOperand(1973)
.Build())
.AddCondition(c =>
c.AsValued(ConditionTypes.ReleaseYear)
.OfDataType<int>()
.WithComparisonOperator(Operators.LesserThanOrEqual)
.SetOperand(1977)
.Build())
.Build();
})
.Build();
RuleBuilder.NewRule<ContentTypes, ConditionTypes>()
.WithName("Benchmark 2 - Bohemian Rapsody")
.WithDateBegin(DateTime.Parse("2000-01-01"))
.WithContent(ContentTypes.Songs, "Bohemian Rapsody")
.WithCondition(x =>
x.And(c => c.Value(ConditionTypes.Artist, Operators.Equal, "Queen")
.Value(ConditionTypes.Lyrics, Operators.Contains, "real life")
.Value(ConditionTypes.ReleaseYear, Operators.GreaterThanOrEqual, 1973)
.Value(ConditionTypes.ReleaseYear, Operators.LesserThanOrEqual, 1977)
)
)
.Build(); What are your thoughts on this? Give it a try first if you want to find any flaws in my suggestion, as I have not tested it 🙂 |
Hello @luispfgarces ! Thanks for your suggestion, I agree it improves the solution. 👌🏼
Given that the method in the RuleBuilder (or RuleBuilderExtension) should only accept one composedCondition, didn't manage to make it exactly how you had describe it (because we should only have one LogicalOperator, which in the proposal is being passed as parameter). I've pushed the new code. Please let me know what you think. |
Hi @jdromeiro, Thank you for taking my input into consideration 👌 if you are interested in resolving that last bit you couldn't resolve, I may have a possible solution for you. The thing is, with the current builders you can't ensure a single "root condition" because on fluent APIs you have to design your interfaces and classes according to the possible state machine to be accomplished. That's why I told you once that fluent APIs require planning representing a state machine on a graph using a tool you'd like (e.g. draw.io) and lots of trial and error sometimes 😄 Here goes my even further improved suggestion:
public interface IRootConditionNodeBuilder<TConditionType>
{
IConfiguredRootConditionNodeBuilder<TConditionType> And(Func<IComposedConditionNodeBuilder<TConditionType>, IComposedConditionNodeBuilder<TConditionType>> conditionFunc);
IConfiguredRootConditionNodeBuilder<TConditionType> Or(Func<IComposedConditionNodeBuilder<TConditionType>, IComposedConditionNodeBuilder<TConditionType>> conditionFunc);
IConfiguredRootConditionNodeBuilder<TConditionType> Value<TDataType>(TConditionType conditionType, Operators condOperator, TDataType operand);
}
public interface IConfiguredRootConditionNodeBuilder<TConditionType>
{
IConditionNode<TConditionType> GetRootCondition();
}
internal class RootConditionNodeBuilder<TConditionType> : IRootConditionNodeBuilder<TConditionType>, IConfiguredRootConditionNodeBuilder<TConditionType>
{
private IConditionNode<TConditionType> rootCondition;
public IConfiguredRootConditionNodeBuilder<TConditionType> And(Func<IComposedConditionNodeBuilder<TConditionType>, IComposedConditionNodeBuilder<TConditionType>> conditionFunc)
{
// Do your already implemented logic. Assign result to rootCondition.
return this;
}
public IConfiguredRootConditionNodeBuilder<TConditionType> Or(Func<IComposedConditionNodeBuilder<TConditionType>, IComposedConditionNodeBuilder<TConditionType>> conditionFunc)
{
// Do your already implemented logic. Assign result to rootCondition.
return this;
}
public IConfiguredRootConditionNodeBuilder<TConditionType> Value<TDataType>(TConditionType conditionType, Operators condOperator, TDataType operand)
{
// Do your already implemented logic. Assign result to rootCondition.
return this;
}
public IConditionNode<TConditionType> GetRootCondition()
=> this.rootCondition;
}
public static IRuleBuilder<TContentType, TConditionType> WithCondition<TContentType, TConditionType>(this IRuleBuilder<TContentType, TConditionType> ruleBuilder, Func<IRootConditionNodeBuilder<TConditionType>, IConfiguredRootConditionNodeBuilder<TConditionType>> conditionFunc) With this, you could accomplish the suggestion I gave you, which has the advantage of defining conditions always the same way (from the consumer's perspective). Maybe this is a bit of nitpicking on my part, but it makes sense to me to offer the best experience possible to the developers that use the fluent API 😄 what do you think? |
@luispfgarces , thank you for your suggestions! 👌🏼 I've taken your snippets and adjusted them a bit. I've also created a new entity IChildComposedConditionNodeBuilder (for which the logic could be kept as the IComposedConditionNodeBuilder) because I thought would be easier to separate the old way from the new way of creating the conditions. If you agree, we can set the old methods as obsolete, and in the future remove them or make them internal. Let me know of your thoughts regarding the solution as it is now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @jdromeiro,
I think you have nailed it this time 💪 I agree with this approach, I'm pro making the current conditions builder API obsolete and using the new improved API you proposed. It would be best if you marked the value condition nodes builder API obsolete also, to state to the consumers "this is the new way to build all the condition nodes for a rule, the old one will be totally deprecated".
I left some comments with small improvements to be made. I will leave my approval to be done later when you have fixed everything (and also leave you the room to refactor anything you see fit if you need to).
Sorry @luispfgarces , decided to also replace all calls to WithContentContainer(). One more 👍🏼 please 😅 |
src/Rules.Framework/Builder/Deprecated/IConditionNodeBuilder.cs
Outdated
Show resolved
Hide resolved
653bdb8
Description
Introduces a new way of more fluently building and adding rule conditions:
It also adds a new
contructor
for the Condition class.Closes #138 .
Change checklist
Please also check the I want to contribute guidelines and make sure you have done accordingly.
Disclaimer
By sending us your contributions, you are agreeing that your contribution is made subject to the terms of our Contributor Ownership Statement