Περιγραφή:
Σε αυτό το άρθρο παρουσιάζουμε πώς να υλοποιήσετε μια καθαρή και ευέλικτη αρχιτεκτονική για την εκτέλεση επιχειρησιακών κανόνων σε μια εφαρμογή C#. Οι κανόνες φορτώνονται δυναμικά από ένα αρχείο JSON και αξιολογούνται χρησιμοποιώντας το Func, αποφεύγοντας έτσι μεγάλα μπλοκ if/else. Η μέθοδος αυτή επιτρέπει εύκολη συντήρηση, προσθήκη εκατοντάδων κανόνων και γρήγορη προσαρμογή της λογικής χωρίς αλλαγές στον κώδικα.
H υλοποίηση του Rule Engine ακολουθεί τις SOLID principles
1 Δημιουργία των Interfaces
public interface IRuleLoader
{
List<RuleDefinition> LoadRules(string path);
}
public interface IRuleEngine
{
decimal CalculateDiscount(Order order, List<RuleDefinition> rules);
}
Αυτό ικανοποιεί ISP και DIP: η υψηλού επιπέδου λογική δεν εξαρτάται από συγκεκριμένες υλοποιήσεις.
2 Models
public class Customer
{
public bool IsVIP { get; set; }
}
public class Order
{
public decimal Total { get; set; }
public Customer Customer { get; set; }
}
public class RuleDefinition
{
public string Name { get; set; } = string.Empty;
public string Condition { get; set; } = string.Empty; // π.χ. "Total > 1000 && Customer.IsVIP"
public decimal Discount { get; set; }
}
3 Loader με JSON
using System.Text.Json;
public class JsonRuleLoader : IRuleLoader
{
public List<RuleDefinition> LoadRules(string path)
{
if (!File.Exists(path))
{
Console.WriteLine($"Το αρχείο κανόνων δεν βρέθηκε: {path}");
return new List<RuleDefinition>();
}
var json = File.ReadAllText(path);
if (string.IsNullOrWhiteSpace(json))
{
Console.WriteLine($"Το αρχείο κανόνων είναι κενό: {path}");
return new List<RuleDefinition>();
}
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
var rules = JsonSerializer.Deserialize<List<RuleDefinition>>(json, options);
return rules ?? new List<RuleDefinition>();
}
}
Τηρεί SRP: μόνο για φόρτωση κανόνων.
4 Rule Engine με Dynamic Expresso
Install-Package DynamicExpresso.Core
using DynamicExpresso;
public class DynamicRuleEngine : IRuleEngine
{
public decimal CalculateDiscount(Order order, List<RuleDefinition> rules)
{
var interpreter = new Interpreter();
interpreter.SetVariable("Total", order.Total);
interpreter.SetVariable("Customer", order.Customer);
foreach (var rule in rules)
{
try
{
// Εκτέλεση συνθήκης από JSON δυναμικά
bool isMatch = interpreter.Eval<bool>(rule.Condition);
if (isMatch)
{
Console.WriteLine($"Ταιριάξε ο κανόνας: {rule.Name}");
return order.Total * rule.Discount;
}
}
catch (Exception ex)
{
Console.WriteLine($"Σφάλμα στην εκτέλεση κανόνα '{rule.Name}': {ex.Message}");
}
}
return 0;
}
}
✅ Τηρεί OCP: νέες συνθήκες προστίθενται στο JSON χωρίς αλλαγή κώδικα.
✅ Τηρεί DIP: το Program δουλεύει με IRuleEngine.
✅ Τηρεί SRP: η κλάση ασχολείται μόνο με εκτέλεση κανόνων.
🔹 Τι είναι η DynamicExpresso
🔹 Τι κάνει ο συγκεκριμένος κώδικας βήμα–βήμα
Ας εξηγήσουμε τη μέθοδο CalculateDiscount:
public decimal CalculateDiscount(Order order, List<RuleDefinition> rules)
{
var interpreter = new Interpreter();
👉 Δημιουργεί έναν Interpreter της DynamicExpresso,
δηλαδή έναν “μηχανισμό που μπορεί να καταλαβαίνει και να εκτελεί C# εκφράσεις”.
interpreter.SetVariable("Total", order.Total);
interpreter.SetVariable("Customer", order.Customer);
👉 Εδώ δηλώνεις μεταβλητές που θα είναι διαθέσιμες μέσα στις εκφράσεις (τους κανόνες).
Δηλαδή, αν έχεις έναν κανόνα:
{ "Condition": "Total > 1000 && Customer.IsVIP" }
τότε η βιβλιοθήκη “ξέρει” τι είναι το Total και τι είναι το Customer.
foreach (var rule in rules)
{
bool isMatch = interpreter.Eval<bool>(rule.Condition);
👉 Για κάθε κανόνα:
- Παίρνει τη συμβολοσειρά (string) του rule.Condition
- Την εκτελεί σαν να ήταν C# κώδικας
- Επιστρέφει true ή false
Παράδειγμα:
"condition": "Total > 1000"
➡️ Η Eval θα επιστρέψει true αν order.Total > 1000, αλλιώς false.
if (isMatch)
{
Console.WriteLine($"Ταιριάξε ο κανόνας: {rule.Name}");
return order.Total * rule.Discount;
}
👉 Αν η συνθήκη ισχύει, εφαρμόζει την έκπτωση του κανόνα.
catch (Exception ex)
{
Console.WriteLine($"Σφάλμα στην εκτέλεση κανόνα '{rule.Name}': {ex.Message}");
}
👉 Αν ο κανόνας έχει λάθος (π.χ. συντακτικό σφάλμα), δεν “σκάει” το πρόγραμμα·
απλώς γράφει μήνυμα λάθους στη κονσόλα.
5 Παράδειγμα JSON (rules.json)
[
{ "Name": "HighValueOrder", "Condition": "Total > 1000", "Discount": 0.10 },
{ "Name": "VIPCustomer", "Condition": "Customer.IsVIP == true", "Discount": 0.20 },
{ "Name": "SmallOrder", "Condition": "Total < 100", "Discount": 0.05 }
]
6 Χρήση στην Main
class Program
{
static void Main()
{
IRuleLoader loader = new JsonRuleLoader();
IRuleEngine engine = new DynamicRuleEngine();
var rules = loader.LoadRules("rules.json");
if (rules.Count == 0)
{
Console.WriteLine("Δεν φορτώθηκαν κανόνες. Τερματισμός.");
return;
}
var order = new Order
{
Total = 1200,
Customer = new Customer { IsVIP = true }
};
var discount = engine.CalculateDiscount(order, rules);
Console.WriteLine($"Έκπτωση που εφαρμόστηκε: {discount:C}");
}
}
Output
✅ Ταιριάξε ο κανόνας: HighValueOrder
💰 Έκπτωση που εφαρμόστηκε: 120,00 €
🔹 Τι κερδίζουμε με αυτή τη SOLID έκδοση
- SRP: Κάθε κλάση έχει μια ευθύνη
- OCP: Νέες συνθήκες προστίθενται στο JSON χωρίς να αλλάζει ο κώδικας
- LSP: Μπορούμε να αντικαταστήσουμε τον IRuleEngine με άλλη υλοποίηση
- ISP: Τα interfaces είναι μικρά και συγκεκριμένα
- DIP: Το υψηλού επιπέδου module (Program) εξαρτάται από abstraction (IRuleEngine, IRuleLoader)
Παρακάτω θα δούμε και ένα βήμα παραπέρα με δυο παραδείγματα cashing:
🔹 Caching των κανόνων (ώστε να μην ξαναφορτώνονται από JSON κάθε φορά)
🔹 Caching των compiled expressions (ώστε να μην ξανα-ερμηνεύονται / ξαναμεταγλωττίζονται κάθε φορά που εκτελείς έναν κανόνα)
🧩 1️⃣ Caching των κανόνων (JSON)
Αυτό είναι απλό και γίνεται στον RuleLoader.
Αν οι κανόνες δεν αλλάζουν συχνά, μπορείς να τους διαβάζεις μία φορά και να τους κρατάς σε στατική μεταβλητή ή memory cache.
public class CachedRuleLoader : IRuleLoader
{
private static List<RuleDefinition>? _cachedRules;
public List<RuleDefinition> LoadRules(string path)
{
if (_cachedRules != null)
return _cachedRules;
if (!File.Exists(path))
{
Console.WriteLine($"Το αρχείο κανόνων δεν βρέθηκε: {path}");
return new List<RuleDefinition>();
}
var json = File.ReadAllText(path);
if (string.IsNullOrWhiteSpace(json))
{
Console.WriteLine($"Το αρχείο κανόνων είναι κενό: {path}");
return new List<RuleDefinition>();
}
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
_cachedRules = JsonSerializer.Deserialize<List<RuleDefinition>>(json, options) ?? new List<RuleDefinition>();
return _cachedRules;
}
}
🟢 Πλεονέκτημα: Το αρχείο διαβάζεται μόνο την πρώτη φορά.
🔴 Μειονέκτημα: Αν αλλάξεις το rules.json, πρέπει να γίνει restart ή refresh.
🧩 2️⃣ Caching των συνθηκών (expressions)
Αυτό είναι το πιο σημαντικό.
Η Dynamic Expresso μεταγλωττίζει (compile) τα expressions κάθε φορά που τα εκτελείς — και αυτό έχει κόστος.
Αν έχεις 500 κανόνες, μπορούμε να κάνουμε caching του compiled delegate (Func) στη μνήμη.
Νέα έκδοση του DynamicRuleEngine με expression cache:
using DynamicExpresso;
using System.Collections.Concurrent;
public class CachedDynamicRuleEngine : IRuleEngine
{
private readonly Interpreter _interpreter;
private readonly ConcurrentDictionary<string, Lambda> _compiledCache;
public CachedDynamicRuleEngine()
{
_interpreter = new Interpreter();
_compiledCache = new ConcurrentDictionary<string, Lambda>();
}
public decimal CalculateDiscount(Order order, List<RuleDefinition> rules)
{
_interpreter.SetVariable("Total", order.Total);
_interpreter.SetVariable("Customer", order.Customer);
foreach (var rule in rules)
{
try
{
// Αν ο κανόνας υπάρχει ήδη στο cache, χρησιμοποίησέ τον
var lambda = _compiledCache.GetOrAdd(rule.Condition, condition =>
_interpreter.Parse(condition, typeof(bool))
);
// Δημιουργία delegate που εκτελείται πάνω σε συγκεκριμένο αντικείμενο
bool isMatch = (bool)lambda.Invoke();
if (isMatch)
{
Console.WriteLine($"Ταιριάξε ο κανόνας: {rule.Name}");
return order.Total * rule.Discount;
}
}
catch (Exception ex)
{
Console.WriteLine($"Σφάλμα στην εκτέλεση κανόνα '{rule.Name}': {ex.Message}");
}
}
return 0;
}
}
Τι κάνει αυτό:
Η πρώτη φορά που τρέχει κάθε Condition → μεταγλωττίζεται και αποθηκεύεται στο _compiledCache.
Οι επόμενες φορές → χρησιμοποιούν τον ίδιο μεταγλωττισμένο delegate (καμία καθυστέρηση).
✅ Εξαιρετικά γρήγορο για πολλά Order αντικείμενα
✅ Τηρεί ακόμη OCP, DIP, SRP
✅ Λειτουργεί για εκατοντάδες κανόνες
💾 Πού γίνεται τελικά το caching;
Τύπος Cache | Πού εφαρμόζεται | Σκοπός |
---|---|---|
Rules cache | Στον RuleLoader
|
Να μη φορτώνονται ξανά οι κανόνες από JSON |
Expression cache | Στον RuleEngine
|
Να μη μεταγλωττίζονται ξανά οι ίδιες συνθήκες |
⚙️ Τελική Σύνοψη
Με αυτό το setup έχεις:
- SOLID αρχιτεκτονική
- Dynamic evaluation των κανόνων
- Caching σε δύο επίπεδα
- Υψηλή απόδοση για 500+ κανόνες και πολλά orders
- Ευκολία επεκτασιμότητας χωρίς να αλλάξεις ούτε μια γραμμή κώδικα όταν προσθέτεις νέο rule
Top comments (0)