Moving Beyond Complex If/Else Logic with NRules and .NET
Business applications are built around rules.
A logistics system may need to determine whether a shipment has missed its terminal appointment. A banking application may decide whether a transaction requires additional approval. An insurance platform may check whether a claim is eligible for coverage. An e-commerce system may calculate discounts based on customer type, order value and promotional conditions.
At the beginning of a project, these decisions are usually implemented using simple C# conditions:
if (shipment.GateAppointmentExpired) {
shipment.Status = "Appointment Missed";
}
There is nothing wrong with this implementation. For a small and stable requirement, an if statement is often the clearest solution.
The difficulty begins when the business process grows.
A shipment may not only have an expired gate appointment. It may also have no ingate transaction, an expired booking, missing documentation, stale GPS information or a temperature violation. Several of these problems may exist at the same time, and each one may require a different response.
The code may gradually become something like this:
if (shipment.GateAppointmentExpired
&& !shipment.HasIngateTransaction
&& shipment.IsAtPort) {
findings.Add("Missed gate appointment");
} else if (!shipment.TerminalIsOpen && shipment.DistanceFromTerminalKm < 5) {
findings.Add("Terminal unavailable");
} else if (shipment.BookingExpiresWithinHours <= 48) {
findings.Add("Booking expiry risk");
} else if (shipment.MissingDocuments.Any()) {
findings.Add("Required documents are missing");
} else if (shipment.CurrentTemperature > shipment.MaximumAllowedTemperature) {
findings.Add("Cold-chain breach");
}
Although this code may initially appear manageable, several problems are already present.
The first problem is that an else if chain normally stops after the first matching condition. A shipment with both an expired gate appointment and missing documents may only receive one finding, even though both issues require attention.
The code could be changed to several independent if statements:
if (shipment.GateAppointmentExpired) {
findings.Add("Missed gate appointment");
}
if (shipment.MissingDocuments.Any()) {
findings.Add("Required documents are missing");
}
if (shipment.CurrentTemperature > shipment.MaximumAllowedTemperature) {
findings.Add("Cold-chain breach");
}
This solves the first-match problem, but it does not solve the larger design problem. As the number of business rules increases, the service becomes responsible for understanding every possible decision in the domain.
A single method may eventually contain dozens or even hundreds of conditions. Some conditions may be repeated across several branches. Rule priorities may become unclear. One change may accidentally affect an unrelated decision. Testing every possible combination becomes increasingly difficult.
Consider a shipment exception diagnosis system. The application may need to evaluate information from several sources:
Shipment and booking details
Gate appointments
Terminal operating status
GPS location and movement history
Driver or vehicle activity
Required documents
Hazardous-material information
Refrigerated cargo temperatures
The decision is no longer a simple sequence such as:
Check condition A.
Otherwise, check condition B.
Otherwise, check condition C.
Instead, the application must reason about a collection of facts:
The shipment is currently near the terminal.
The gate appointment has expired.
No ingate transaction has been recorded.
The booking expires within the next 48 hours.
The GPS location has not been updated recently.
Two required documents are missing.
Different business rules may evaluate different combinations of these facts. More than one rule may apply to the same shipment.
For example:IF the shipment is at the port
AND the gate appointment has expired
AND no ingate transaction exists
THEN identify a missed gate appointment.Another independent rule may state:
IF required shipment documents are missing
THEN identify a documentation problem.A third rule may state:
IF refrigerated cargo exceeds its permitted temperature
THEN identify a cold-chain breach
AND require manual review.
All three rules could apply to the same shipment. The final diagnosis should therefore contain all three findings rather than selecting only one.
This is where the idea of a rules engine becomes useful.A rules engine allows us to separate business knowledge into individual rules. Instead of placing every condition inside one large application service, each rule describes a specific business situation and the action that should be taken when that situation occurs.The application provides the available data as facts. The rules engine evaluates those facts against the registered rules. When a rule's conditions are satisfied, the rule fires and produces a finding, recommendation, decision or additional fact.
The overall process looks like this:Application data ↓Domain facts ↓Rules engine ↓Matching rules fire ↓Findings and recommended actions
The objective is not to remove every if, else or switch statement from the application. Conditional statements remain useful for small technical decisions and straightforward workflows.The objective is to prevent complex, frequently changing business knowledge from being concentrated inside large procedural methods.A rules-engine approach becomes especially valuable when:
- The system contains many independent business rules.
- Multiple rules may apply to the same case.
- Rules combine information from different domain objects.
- The business requires an explanation for each decision.
- Rules change more frequently than the surrounding application.
- New rules must be added without modifying a large central method.
- Rules may produce additional facts that activate other rules.
In this article, we will explore these ideas using a practical ASP.NET Core Web API built with NRules. The demo project diagnoses shipment exceptions by evaluating facts related to bookings, gate appointments, terminals, GPS information, documents and cargo temperatures.
We will begin with the fundamentals of business rules, introduce expert systems and inference engines, examine the main concepts behind NRules, and then walk through the shipment exception diagnosis project step by step.What Is a Business Rule?
A business rule is a statement that defines how an organization expects a process, decision, or operation to behave.It describes what should happen when certain conditions are true.Business rules can come from many sources, including:- Company policies
- Operational procedures
- Government regulations
- Industry standards
- Customer agreements
- Safety requirements
- Approval workflows
- Domain knowledge gathered from experienced employees
A business rule is usually expressed in a simple structure:IF a condition is true
THEN perform an action or reach a conclusion.
For example:
IF an order value exceeds $10,000 THEN manager approval is required.
This rule contains two main parts:Condition: The order value exceeds $10,000. Action: Require manager approval.In a logistics system, a rule may look like this:
AND no ingate transaction has been recorded
THEN identify a missed gate appointment.
Here, the rule combines two conditions before producing a conclusion.Business rules do not always produce a simple yes-or-no result. Depending on the application, a rule may:- Approve or reject a request
- Assign a risk level
- Add a warning
- Calculate a charge
- Recommend an action
- Escalate an issue
- Request manual review
- Create another fact for further evaluation
- Trigger a technical process such as sending a notification
For example:
THEN classify the issue as a cold-chain breach
AND require manual review.
This rule produces more than one outcome. It classifies the problem and changes the next step in the workflow.Business Rules Versus Technical Rules
It is useful to distinguish business rules from technical implementation rules.A business rule represents domain knowledge:A hazardous shipment must include a hazardous-material declaration.A technical rule controls how the software operates:Retry the API request three times when a network timeout occurs.Both may be implemented using conditional logic, but they serve different purposes.The first rule belongs to the logistics domain. It may be discussed and approved by operations specialists, compliance officers, or business analysts.The second belongs to the application infrastructure. It is mainly decided by software developers or system architects.This distinction matters because business rules usually change for business reasons. A carrier may change its booking policy. A regulator may introduce a new document requirement. An operations team may adjust the time threshold used for escalation.When such rules are mixed deeply into technical code, they become harder to identify, review, and update.Examples of Business Rules
Business rules exist in almost every software system.Banking
IF a transaction exceeds the customer's normal spending patternTHEN request additional verification.Insurance
IF the policy was inactive on the accident dateTHEN the claim is not eligible for coverage.E-commerce
IF the customer is a premium memberAND the order value exceeds $100THEN apply free delivery.Human resources
IF an employee requests more than five consecutive leave daysTHEN department-head approval is required.Healthcare
IF a patient has a critical test resultTHEN notify the responsible medical team immediately.Logistics
IF a booking expires within the next 48 hoursAND the shipment has not yet entered the terminalTHEN flag the shipment for operational attention.These rules are understandable even without seeing the software implementation. That is an important characteristic of a good business rule.Declarative and Procedural Thinking
Business rules are often easier to understand when written declaratively.A declarative rule states what should be true or what should happen:
THEN create a documentation warning.
It does not describe every technical step required to check the database, read the request, create an object, or return an API response.Procedural code, on the other hand, explains how the program should execute:if (shipment.Documents is not null) { var missingDocuments = shipment.Documents .Where(document => !document.IsAvailable).ToList();
if (missingDocuments.Count > 0) { diagnosis.Findings.Add(new DiagnosisFinding { Code = "MISSING_DOCUMENTS", Severity = Severity.High }); }}The procedural implementation is necessary, but the underlying business rule is much simpler:
THEN create a high-severity documentation finding.
A rules engine allows the implementation to remain closer to this declarative style.Atomic Business Rules
A good rule should normally represent one clear piece of business knowledge.For example:
AND no ingate transaction exists
THEN identify a missed gate appointment.
This is preferable to creating one large rule that tries to handle every possible shipment problem:
OR the terminal is closed
OR the GPS is stale
OR documents are missing
OR the temperature is too high
THEN mark the shipment as problematic.
The second rule loses important information. It tells us that something is wrong, but it does not clearly identify what happened or what action should follow.By separating the knowledge into smaller rules, the system can produce multiple precise findings:- Missed gate appointment
- Terminal unavailable
- Stale GPS information
- Missing documentation
- Cold-chain breach
Each finding can have its own:
- Severity
- Evidence
- Recommended action
- Responsible team
- Escalation path
Rules Can Overlap
Business rules are not always mutually exclusive.A single shipment may satisfy several rules at the same time.Consider this situation:The gate appointment expired. No ingate transaction exists. The terminal is currently closed. The booking expires tomorrow. The latest GPS update is six hours old.This information may activate several independent rules:TerminalUnavailableRuleMissedGateAppointmentRuleBookingExpiryRiskRuleStaleGpsSignalRuleThe final result should combine these findings instead of selecting only one.This is one of the main differences between a rule-based system and a traditional decision tree. A decision tree often follows one branch at a time. A rule-based system can evaluate many independent rules against the same facts.Rules Need Clear Terminology
Before implementing business rules, the domain terms must be clearly defined.For the shipment exception project, important terms include:Shipment booking
A reservation with a carrier or terminal that allows cargo to be transported or processed.Gate appointment
A scheduled time slot for a truck to enter a terminal and deliver or collect a container.Ingate transaction
A system record confirming that the truck or container has entered the terminal.Booking expiry
The time after which the booking is no longer valid.Hazardous cargo
Cargo that may create health, safety, environmental, or operational risks and therefore requires special handling and documentation.Cold chain
A temperature-controlled transportation process used for goods such as food, medicine, or chemicals.Stale GPS data
Location information that has not been updated within the expected time period.Without clear definitions, developers and business users may interpret the same rule differently.For example:
THEN create a warning.
This rule is incomplete until the business defines what "stale" means.It may mean:No GPS update for more than 30 minutes.Or:No GPS update for more than four hours.The correct threshold depends on the operating environment.Rules Must Be Testable
A business rule should be written precisely enough to test.Consider this rule:
THEN raise a warning.
The phrase "about to expire" is ambiguous.A more testable rule is:
AND the booking has not already expired
THEN raise a booking-expiry warning.
Now developers can create clear test cases:Booking expires in 24 hours→ Rule should matchBooking expires in 72 hours→ Rule should not matchBooking expired yesterday→ A different rule may applyPrecise rules make both implementation and testing easier.Business Rules in the Demo Project
The shipment exception demo uses business rules such as:IF the shipment is at the portAND the gate appointment has expiredAND no ingate transaction existsTHEN identify a missed gate appointment.
IF the vehicle has not moved for a defined periodAND the ignition is offAND the vehicle is away from a terminalTHEN identify a possible vehicle breakdown.
IF the terminal is closedAND the shipment is near that terminalTHEN identify terminal unavailability.
IF required documents are missingTHEN identify a documentation problem.
IF the cargo is refrigeratedAND its current temperature exceeds the permitted limitTHEN identify a cold-chain breach.These rules are independent, but they contribute to the same final shipment diagnosis.The application does not need to decide which single rule is the most important before evaluation begins. It provides the available facts, and the rules engine determines which rules match.
From Plain-Language Rules to C# Code
Once a business rule is defined clearly, it can usually be implemented with a normal C# condition.Consider this rule:
AND the gate appointment has expired
AND no ingate transaction exists
THEN identify a missed gate appointment.
A simple C# implementation could be:if (shipment.IsAtPort && shipment.GateAppointmentEndUtc < DateTime.UtcNow && !shipment.HasIngateTransaction){ diagnosis.AddFinding( DiagnosisFinding.MissedGateAppointment());}For a small number of rules, this is perfectly acceptable. The code is easy to read, test, and debug.The difficulty begins when the system contains many independent rules:if (shipment.GateAppointmentExpired) { diagnosis.AddFinding(DiagnosisFinding.MissedGateAppointment());}if (!shipment.TerminalIsOpen) { diagnosis.AddFinding(DiagnosisFinding.TerminalUnavailable());}if (shipment.MissingDocuments.Count > 0) { diagnosis.AddFinding(DiagnosisFinding.MissingDocuments());}if (shipment.HasColdChainBreach) { diagnosis.AddFinding(DiagnosisFinding.ColdChainBreach());}This still works, but one service gradually becomes responsible for every business rule in the shipment domain. As more rules are added, the method becomes harder to maintain and test.A better first step is to move each rule into its own C# class:public interface IShipmentRule{ bool IsMatch(Shipment shipment); DiagnosisFinding CreateFinding( Shipment shipment);}public sealed class MissingDocumentsRule : IShipmentRule{ public bool IsMatch(Shipment shipment) { return shipment.MissingDocuments.Count > 0; }
public DiagnosisFinding CreateFinding( Shipment shipment) { return DiagnosisFinding.MissingDocuments(); }}The application can then evaluate all registered rules:foreach (var rule in rules){ if (rule.IsMatch(shipment)) { diagnosis.AddFinding( rule.CreateFinding(shipment)); }}This design is cleaner because every rule has one responsibility and can be tested independently.However, shipment data often comes from different sources, such as GPS systems, terminal APIs, booking services, document systems, and temperature sensors. Instead of storing everything in one large object, the demo project divides the data into smaller objects called facts:
- ShipmentFact
- LocationFact
- GateAppointmentFact
- TerminalFact
- BookingFact
- DocumentationFact
- TemperatureFact
Each rule only uses the facts it needs. For example, a missed gate appointment rule may use shipment, location, and appointment facts, while a cold-chain rule only needs shipment and temperature facts.Normal C# can still evaluate these facts manually, but the application must find the correct objects, match them using the shipment ID, evaluate every condition, and track which rules fired.A rules engine such as NRules handles this matching and execution process for us.The key point is not that if/else is wrong. Simple rules should remain simple C# code. A rules engine becomes useful when many independent rules evaluate different combinations of facts and several rules may apply to the same case.
What Is an Expert System?
An expert system is a software system that applies domain knowledge to make decisions, identify problems, or recommend actions.It usually contains three main parts:Facts ↓Rules ↓Inference EngineFacts represent what the system currently knows.In the shipment project, facts may include:
The gate appointment has expired.
No ingate transaction exists.
Required documents are missing.
Rules represent the knowledge used to evaluate those facts:
AND the gate appointment has expired
AND no ingate transaction exists
THEN identify a missed gate appointment.
The inference engine matches the available facts against the rules and determines which rules should execute.When a rule's conditions are satisfied, the rule fires. Its action may then:Add a diagnosis findingAssign a severityRecommend an actionRequest manual reviewProduce another factFor example:Matched facts:
Appointment has expired
No ingate transaction exists
Result:
Severity: High
Recommended action: Arrange a new appointment
Several rules can fire for the same shipment. A shipment may have a missed appointment, missing documents, and a temperature breach at the same time.This is different from a simple decision tree that follows only one path. An expert system combines the results of all relevant rules to produce a more complete diagnosis.The system is called an "expert" system because its rules represent knowledge that would normally come from people experienced in the domain, such as logistics operators, compliance officers, or terminal specialists.It is important to note that an expert system does not necessarily use machine learning or an LLM. A rule-based expert system is normally deterministic: the same facts and rules produce the same result.
Main Features of an Expert System
An expert system is more than a collection of conditional statements. It provides a structured way to represent knowledge and apply it consistently.Knowledge represented as rules
Domain knowledge is separated into individual rules:
THEN raise a booking-expiry warning.
This makes each rule easier to understand, review, and test.Fact-based reasoning
The system evaluates facts that describe the current situation, such as:
Terminal status
Latest GPS update
Missing documents
Cargo temperature
Different rules can use different combinations of these facts.Multiple rules can fire
The engine does not have to stop after finding the first issue. Several rules may contribute to the same diagnosis.For example, one shipment could produce:
Missing documentation
Cold-chain breach
Explainable results
A useful expert system should explain why it reached a conclusion.The shipment project returns:The detected problem
Its severity
Supporting evidence
Recommended action
The rule that firedConsistent decisions
The same facts and rules produce the same outcome. This helps ensure that business policies are applied consistently across requests.Forward chaining
A fired rule can create a new fact that activates another rule.
↓
CriticalShipmentFact created
↓
ManualReviewRule fires
This allows the system to build conclusions step by step.Separation of knowledge and execution
The rules decide what should happen, while application services perform technical actions such as sending emails, updating records, or creating review tasks.This separation keeps business knowledge easier to maintain without mixing it with infrastructure code.Implementing the Expert System with NRules
NRules is a .NET rules engine that allows business rules to be written as separate C# classes. Each rule defines the facts it requires, the conditions that must match, and the action to execute.In the shipment project, the application first converts the incoming request into facts:
request.ShipmentId,
request.EvaluationTimeUtc);
var location = new LocationFact(
request.ShipmentId,
request.IsAtPort,
request.DistanceFromTerminalKm,
request.LastGpsUpdateUtc,
request.LastMovementUtc,
request.IgnitionOn);
A new NRules session is then created:
var session = _sessionFactory.CreateSession();
The facts and diagnosis object are inserted into the session:
session.Insert(location);
session.Insert(gateAppointment);
session.Insert(terminal);
session.Insert(booking);
session.Insert(documentation);
session.Insert(temperature);
session.Insert(diagnosis);
The application then asks NRules to evaluate the facts:
session.Fire();
Calling Fire() causes NRules to find every rule whose conditions are satisfied and execute its action.For example, the missed gate appointment rule may look like this:public sealed class MissedGateAppointmentRule : Rule{ public override void Define() { ShipmentFact shipment = default!; LocationFact location = default!; GateAppointmentFact appointment = default!; ShipmentDiagnosis diagnosis = default!; When() .Match(() => shipment) .Match(() => location, x => x.ShipmentId == shipment.ShipmentId, x => x.IsAtPort) .Match(() => appointment, x => x.ShipmentId == shipment.ShipmentId, x => x.EndUtc < shipment.EvaluationTimeUtc, x => !x.HasIngateTransaction) .Match(() => diagnosis, x => x.ShipmentId == shipment.ShipmentId); Then() .Do(_ => diagnosis.AddFinding( DiagnosisFinding.MissedGateAppointment())); }}The When() section defines the conditions. The Then() section defines what happens when those conditions match.After all matching rules fire, the diagnosis contains the combined findings:return new ShipmentDiagnosisResponse( diagnosis.ShipmentId, diagnosis.OverallSeverity, diagnosis.RequiresManualReview, diagnosis.Findings, diagnosis.FiredRules);The overall flow is:
↓
Create domain facts
↓
Insert facts into NRules session
↓
Call session.Fire()
↓
Matching rules execute
↓
Return combined diagnosis
This keeps the controller and application service simple while allowing each business rule to remain independent and testable.
How NRules Sessions and Facts Work
An NRules session represents one rule-evaluation cycle.For each shipment diagnosis, the application creates a new session:
var session = _sessionFactory.CreateSession();
The session contains the working memory of the rules engine. This is where the application inserts the facts that describe the current shipment.
session.Insert(location);
session.Insert(gateAppointment);
session.Insert(terminal);
session.Insert(booking);
session.Insert(documentation);
session.Insert(temperature);
session.Insert(diagnosis);
Each fact represents one area of the domain:
LocationFact → GPS location and movement information
GateAppointmentFact → Appointment and ingate status
TerminalFact → Terminal availability
BookingFact → Booking validity and expiry
DocumentationFact → Missing required documents
NRules compares these facts with the conditions defined inside every rule.For example, a rule may require:
- A ShipmentFact
- A LocationFact for the same shipment
- A GateAppointmentFact for the same shipment
The facts are connected using ShipmentId:
x => x.ShipmentId == shipment.ShipmentId)
This prevents the engine from accidentally combining information belonging to different shipments.Once all facts have been inserted, the application calls:
session.Fire();
NRules then identifies the matching rules and executes their actions. More than one rule can fire during the same session.The compiled rule structure is normally reused across the application, but each request receives a new session. This keeps the facts and results of one shipment diagnosis separate from another.
↓
Session for Shipment A
↓
Facts and findings for Shipment A
Compiled rules
↓
Session for Shipment B
↓
Facts and findings for Shipment B
In simple terms, the session is the temporary workspace where NRules receives facts, evaluates rules, and produces the diagnosis for one request.
Understanding When(), Match(), and Then()
An NRules rule is mainly divided into two parts:WHEN certain facts satisfy the conditionsTHEN perform an actionIn code, this is expressed using When(), Match(), and Then().Consider the missed gate appointment rule:
{
ShipmentFact shipment = default!;
LocationFact location = default!;
GateAppointmentFact appointment = default!;
ShipmentDiagnosis diagnosis = default!;
When()
.Match(() => shipment)
.Match(() => location,
x => x.ShipmentId == shipment.ShipmentId,
x => x.IsAtPort)
.Match(() => appointment,
x => x.ShipmentId == shipment.ShipmentId,
x => x.EndUtc < shipment.EvaluationTimeUtc,
x => !x.HasIngateTransaction)
.Match(() => diagnosis,
x => x.ShipmentId == shipment.ShipmentId);
Then()
.Do(_ => diagnosis.AddFinding(
DiagnosisFinding.MissedGateAppointment()));
}
When()
When() starts the condition section of the rule.It essentially means:Evaluate the following facts and conditions.Match()
Each Match() tells NRules what type of fact the rule needs and what conditions that fact must satisfy.For example:
x => x.IsAtPort)
means:Find a LocationFact where the shipment is currently at the port.We can also relate facts to each other:
x => x.ShipmentId == shipment.ShipmentId
This ensures that the location and shipment facts belong to the same shipment.Then()
Then() defines what should happen after all conditions match.
.Do(_ => diagnosis.AddFinding(
DiagnosisFinding.MissedGateAppointment()));
At this point, the rule fires and adds a new finding to the diagnosis.So the rule can be read almost like plain English:
a shipment exists,
the shipment is at the port,
its gate appointment has expired,
and no ingate transaction exists
THEN
add a missed gate appointment finding.
This separation between conditions and actions is one of the main reasons rules are easier to understand than large blocks of procedural business logic.
How Multiple NRules Rules Contribute to One Diagnosis
One of the main advantages of the rules-engine approach is that several independent rules can contribute to the same result.For example, a shipment may simultaneously have:
Missing documents
A stale GPS signal
A cold-chain temperature breach
Each issue is handled by a separate rule:
MissingDocumentationRule
StaleGpsSignalRule
ColdChainBreachRule
When the session fires, NRules evaluates all of them:
session.Fire();
If several rule conditions are satisfied, several rules can fire during the same session.Each rule adds its own finding:
diagnosis.AddFinding(...);
The final diagnosis may therefore contain multiple results:
High → Missed gate appointment
High → Missing documentation
Medium → GPS data is stale
Critical → Cold-chain breach
The system can then calculate an overall severity and determine whether manual review is required.This is different from a traditional if / else if chain, where execution may stop after the first matching condition.With NRules, each business rule remains independent, while all matching findings are combined into one complete diagnosis.
Walking Through the Main Rules in the Demo Project
The demo project contains several independent rules, each responsible for detecting one type of shipment exception.Missed Gate Appointment
This rule checks whether the shipment is at the port, the gate appointment has already expired, and no ingate transaction has been recorded.
AND appointment has expired
AND no ingate transaction exists
THEN report a missed gate appointment.
Possible Vehicle Breakdown
This rule uses location and vehicle activity information to identify a vehicle that may have stopped unexpectedly.
AND ignition is off
AND the vehicle is away from the terminal
THEN report a possible breakdown.
Terminal Unavailable
A shipment may arrive near a terminal while the terminal is closed or unavailable.
AND terminal is closed
THEN report terminal unavailability.
Booking Expiry Risk
This rule identifies shipments whose booking is close to expiry and may require operational attention.
AND expiry is approaching
THEN report a booking-expiry risk.
Missing Documentation
Required documents are important for processing shipments, especially regulated or hazardous cargo.
THEN report a documentation exception.
Stale GPS Signal
The system can also detect when location information has not been updated recently.
THEN report stale GPS information.
Cold-Chain Breach
For refrigerated cargo, the system compares the current temperature with the permitted limit.
AND temperature exceeds the permitted limit
THEN report a cold-chain breach.
The important point is that these rules do not compete with each other. A single shipment can satisfy several of them, allowing the final diagnosis to represent the complete situation rather than only the first detected problem.
C# vs NRules: Side-by-Side Comparison
The same business rule can be implemented using normal C# or NRules.Consider this rule:
AND the gate appointment has expired
AND no ingate transaction exists
THEN report a missed gate appointment.
Normal C#
appointment.EndUtc < shipment.EvaluationTimeUtc &&
!appointment.HasIngateTransaction)
{
diagnosis.AddFinding(
DiagnosisFinding.MissedGateAppointment());
}
This is simple and works well when the number of rules is small.NRules
.Match(() => shipment)
.Match(() => location,
x => x.ShipmentId == shipment.ShipmentId,
x => x.IsAtPort)
.Match(() => appointment,
x => x.ShipmentId == shipment.ShipmentId,
x => x.EndUtc < shipment.EvaluationTimeUtc,
x => !x.HasIngateTransaction);
Then()
.Do(_ => diagnosis.AddFinding(
DiagnosisFinding.MissedGateAppointment()));
The NRules version is more verbose for a single rule, so there is little benefit if the application only has a few conditions.The advantage appears when the system grows to dozens of independent rules. Each rule can live in its own class, use only the facts it needs, and fire independently without expanding one large decision method.In short:Simple and stable logic→ Normal C# is usually enough.Many independent and interacting rules→ A rules engine becomes easier to manage.The goal is not to replace every if statement. It is to keep growing business knowledge separated from the application's procedural flow.
What Happens After a Rule Fires?
When an NRules rule matches, it does not simply return true. Its Then() block executes.For example:
.Do(_ => diagnosis.AddFinding(
DiagnosisFinding.MissedGateAppointment()));
Here, the rule fires and adds a finding to the current diagnosis.A rule action could also produce something that changes the application's next step, such as:
ManualReviewAction
HoldShipmentAction
A good design is to let the rules engine decide what should happen, while the application handles how it happens.
↓
Rule matches
↓
Rule fires
↓
Produces finding or action
↓
Application executes the action
For example, a critical cold-chain rule could produce a manual-review action:
.Do(_ =>
{
diagnosis.AddFinding(
DiagnosisFinding.ColdChainBreach());
diagnosis.AddAction(
new ManualReviewAction(
shipment.ShipmentId,
"Critical temperature violation"));
});
The application can then pass that action to the appropriate handler, which may create a review task, send an email, or update another system.This keeps responsibilities separate:Rules engine: decides what should happen.Application layer: coordinates the workflow.Handlers/services: perform technical operations.So the rules engine can influence application execution without placing email, database, or external API logic directly inside every rule.Limitations and Practical Considerations of NRules
NRules can make complex business logic easier to organize, but it also introduces additional concepts and infrastructure that developers need to understand.Rules Are Still Code
NRules rules are normally written using its C# DSL. This means changing a rule usually requires modifying code, testing it, and deploying the application again. NRules can externalize its rule model using JSON, but doing so introduces additional complexity and security considerations.Rule Execution Is Less Procedural
With normal C# code, we can usually follow execution from one statement to the next.With NRules, there is no fixed sequence such as:Rule A → Rule B → Rule CInstead, the engine determines which rules are activated from the available facts and resolves which activated rules execute.This can make debugging unfamiliar rule sets more challenging.Session Management Matters
Each ISession contains its own working memory. Multiple sessions can share the same compiled rule network while keeping their facts independent.For a Web API, a common pattern is therefore:
↓
Reuse ISessionFactory
↓
Create a new ISession per evaluation
Avoid Heavy Side Effects Inside Rules
Although NRules supports injecting dependencies into rules, placing database updates, email delivery, or external API calls directly inside every rule can make retrying and testing more difficult.A cleaner approach is often:
↓
Produces a finding or action
↓
Application service performs it
NRules is therefore most valuable when the complexity of the business rules justifies the additional abstraction. For simple business logic, ordinary C# remains the better choice.Conclusion
Business rules are unavoidable in most enterprise applications. The challenge is not writing the first few rules - it is keeping them understandable and maintainable as the number of rules grows.For simple logic, normal C# if/else conditions are often the best choice. But when many independent rules evaluate different combinations of domain data, several rules can apply at the same time, and decisions need to be explainable, a rules engine can provide a cleaner structure.In our shipment exception example, we divided operational data into smaller domain objects called facts, inserted those facts into an NRules session, and allowed independent rules to evaluate them. Matching rules then fired and contributed findings, severity levels, evidence, and recommended actions to a combined diagnosis.The overall idea is simple:
↓
Facts
↓
Rules
↓
Matching rules fire
↓
Findings / Decisions / Actions
NRules does not eliminate conditional logic, nor should it. Instead, it provides a way to separate complex business knowledge from procedural application code.The key is knowing when that additional structure is justified.Simple business logic→ Keep it simple with C#Complex, growing, independent rules→ Consider a rules engineUsed in the right situations, rule engines can make business logic easier to extend, test, explain, and maintain as an application evolves...
