Saya membangun mesin aturan yang menggunakan pendekatan berbeda dari yang Anda uraikan dalam pertanyaan Anda, tetapi saya pikir Anda akan menemukan itu jauh lebih fleksibel daripada pendekatan Anda saat ini.
Pendekatan Anda saat ini tampaknya difokuskan pada satu entitas, "Pengguna", dan aturan Anda yang tetap mengidentifikasi "propertyname", "operator" dan "value". Pola saya, sebagai gantinya menyimpan kode C # untuk predikat (Func <T, bool>) dalam kolom "Ekspresi" di database saya. Dalam desain saat ini, menggunakan pembuatan kode saya menanyakan "aturan" dari database saya dan mengkompilasi perakitan dengan tipe "Aturan", masing-masing dengan metode "Uji". Berikut adalah tanda tangan untuk antarmuka yang diterapkan setiap Aturan:
public interface IDataRule<TEntity>
{
/// <summary>
/// Evaluates the validity of a rule given an instance of an entity
/// </summary>
/// <param name="entity">Entity to evaluate</param>
/// <returns>result of the evaluation</returns>
bool Test(TEntity entity);
/// <summary>
/// The unique indentifier for a rule.
/// </summary>
int RuleId { get; set; }
/// <summary>
/// Common name of the rule, not unique
/// </summary>
string RuleName { get; set; }
/// <summary>
/// Indicates the message used to notify the user if the rule fails
/// </summary>
string ValidationMessage { get; set; }
/// <summary>
/// indicator of whether the rule is enabled or not
/// </summary>
bool IsEnabled { get; set; }
/// <summary>
/// Represents the order in which a rule should be executed relative to other rules
/// </summary>
int SortOrder { get; set; }
}
"Ekspresi" dikompilasi sebagai tubuh dari metode "Uji" ketika aplikasi pertama kali dieksekusi. Seperti yang Anda lihat, kolom lain dalam tabel juga ditampilkan sebagai properti kelas satu pada aturan sehingga pengembang memiliki fleksibilitas untuk menciptakan pengalaman tentang bagaimana pengguna mendapat pemberitahuan tentang kegagalan atau kesuksesan.
Menghasilkan perakitan dalam memori adalah kejadian 1 kali selama aplikasi Anda dan Anda mendapatkan peningkatan kinerja dengan tidak harus menggunakan refleksi saat mengevaluasi aturan Anda. Ekspresi Anda diperiksa pada saat runtime karena majelis tidak akan menghasilkan dengan benar jika nama properti salah eja, dll.
Mekanisme pembuatan rakitan di dalam memori adalah sebagai berikut:
- Muat aturan Anda dari DB
- beralih pada aturan dan untuk masing-masing, menggunakan StringBuilder dan beberapa rangkaian string menulis Teks yang mewakili kelas yang mewarisi dari IDataRule
- kompilasi menggunakan CodeDOM - info lebih lanjut
Ini sebenarnya cukup sederhana karena sebagian besar kode ini adalah implementasi properti dan inisialisasi nilai dalam konstruktor. Selain itu, satu-satunya kode lainnya adalah Ekspresi.
CATATAN: ada batasan bahwa ekspresi Anda harus. NET 2.0 (tidak ada fitur lambdas atau C # 3.0 lainnya) karena keterbatasan dalam CodeDOM.
Berikut ini beberapa contoh kode untuk itu.
sb.AppendLine(string.Format("\tpublic class {0} : SomeCompany.ComponentModel.IDataRule<{1}>", className, typeName));
sb.AppendLine("\t{");
sb.AppendLine("\t\tprivate int _ruleId = -1;");
sb.AppendLine("\t\tprivate string _ruleName = \"\";");
sb.AppendLine("\t\tprivate string _ruleType = \"\";");
sb.AppendLine("\t\tprivate string _validationMessage = \"\";");
/// ...
sb.AppendLine("\t\tprivate bool _isenabled= false;");
// constructor
sb.AppendLine(string.Format("\t\tpublic {0}()", className));
sb.AppendLine("\t\t{");
sb.AppendLine(string.Format("\t\t\tRuleId = {0};", ruleId));
sb.AppendLine(string.Format("\t\t\tRuleName = \"{0}\";", ruleName.TrimEnd()));
sb.AppendLine(string.Format("\t\t\tRuleType = \"{0}\";", ruleType.TrimEnd()));
sb.AppendLine(string.Format("\t\t\tValidationMessage = \"{0}\";", validationMessage.TrimEnd()));
// ...
sb.AppendLine(string.Format("\t\t\tSortOrder = {0};", sortOrder));
sb.AppendLine("\t\t}");
// properties
sb.AppendLine("\t\tpublic int RuleId { get { return _ruleId; } set { _ruleId = value; } }");
sb.AppendLine("\t\tpublic string RuleName { get { return _ruleName; } set { _ruleName = value; } }");
sb.AppendLine("\t\tpublic string RuleType { get { return _ruleType; } set { _ruleType = value; } }");
/// ... more properties -- omitted
sb.AppendLine(string.Format("\t\tpublic bool Test({0} entity) ", typeName));
sb.AppendLine("\t\t{");
// #############################################################
// NOTE: This is where the expression from the DB Column becomes
// the body of the Test Method, such as: return "entity.Prop1 < 5"
// #############################################################
sb.AppendLine(string.Format("\t\t\treturn {0};", expressionText.TrimEnd()));
sb.AppendLine("\t\t}"); // close method
sb.AppendLine("\t}"); // close Class
Di luar ini saya membuat kelas yang saya sebut "DataRuleCollection", yang mengimplementasikan ICollection>. Ini memungkinkan saya untuk membuat kemampuan "TestAll" dan pengindeks untuk mengeksekusi aturan tertentu dengan nama. Berikut ini implementasi untuk kedua metode tersebut.
/// <summary>
/// Indexer which enables accessing rules in the collection by name
/// </summary>
/// <param name="ruleName">a rule name</param>
/// <returns>an instance of a data rule or null if the rule was not found.</returns>
public IDataRule<TEntity, bool> this[string ruleName]
{
get { return Contains(ruleName) ? list[ruleName] : null; }
}
// in this case the implementation of the Rules Collection is:
// DataRulesCollection<IDataRule<User>> and that generic flows through to the rule.
// there are also some supporting concepts here not otherwise outlined, such as a "FailedRules" IList
public bool TestAllRules(User target)
{
rules.FailedRules.Clear();
var result = true;
foreach (var rule in rules.Where(x => x.IsEnabled))
{
result = rule.Test(target);
if (!result)
{
rules.FailedRules.Add(rule);
}
}
return (rules.FailedRules.Count == 0);
}
LEBIH KODE: Ada permintaan untuk kode yang terkait dengan Pembuatan Kode. Saya merangkum fungsionalitas dalam kelas yang disebut 'RulesAssemblyGenerator' yang telah saya sertakan di bawah ini.
namespace Xxx.Services.Utils
{
public static class RulesAssemblyGenerator
{
static List<string> EntityTypesLoaded = new List<string>();
public static void Execute(string typeName, string scriptCode)
{
if (EntityTypesLoaded.Contains(typeName)) { return; }
// only allow the assembly to load once per entityType per execution session
Compile(new CSharpCodeProvider(), scriptCode);
EntityTypesLoaded.Add(typeName);
}
private static void Compile(CodeDom.CodeDomProvider provider, string source)
{
var param = new CodeDom.CompilerParameters()
{
GenerateExecutable = false,
IncludeDebugInformation = false,
GenerateInMemory = true
};
var path = System.Reflection.Assembly.GetExecutingAssembly().Location;
var root_Dir = System.IO.Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "Bin");
param.ReferencedAssemblies.Add(path);
// Note: This dependencies list are included as assembly reference and they should list out all dependencies
// That you may reference in your Rules or that your entity depends on.
// some assembly names were changed... clearly.
var dependencies = new string[] { "yyyyyy.dll", "xxxxxx.dll", "NHibernate.dll", "ABC.Helper.Rules.dll" };
foreach (var dependency in dependencies)
{
var assemblypath = System.IO.Path.Combine(root_Dir, dependency);
param.ReferencedAssemblies.Add(assemblypath);
}
// reference .NET basics for C# 2.0 and C#3.0
param.ReferencedAssemblies.Add(@"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.dll");
param.ReferencedAssemblies.Add(@"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.5\System.Core.dll");
var compileResults = provider.CompileAssemblyFromSource(param, source);
var output = compileResults.Output;
if (compileResults.Errors.Count != 0)
{
CodeDom.CompilerErrorCollection es = compileResults.Errors;
var edList = new List<DataRuleLoadExceptionDetails>();
foreach (CodeDom.CompilerError s in es)
edList.Add(new DataRuleLoadExceptionDetails() { Message = s.ErrorText, LineNumber = s.Line });
var rde = new RuleDefinitionException(source, edList.ToArray());
throw rde;
}
}
}
}
Jika ada lain pertanyaan atau komentar atau permintaan untuk sampel kode lebih lanjut, biarkan aku tahu.