Dynamic Menu Data Sources. Finally! We know that a lot of you guys have been waiting for this feature. I believe that with this implementation we addressed most of your suggestions and requests on how it should work. This is what we came up with:
Dynamic Menu Data Sources are useful when you have a dynamic list of key/value pairs and would like to use those values in Rule Editor as if they were members of a plain .NET enumerator.
For example, let's say that your database has a table called Physician that lists some doctors and their IDs. In your source object you'd have a property PhysicianID of type System.Int32. You would like to use those doctors in your rules but you don't want rule authors to type in doctor names or display a menu of their IDs to choose from. You'd rather prefer rule authors to be able to select the doctor's name from a menu but store the ID of the selected doctor in the rule XML. The new feature allows you to do just that.
You can define the dynamic source for doctor's menu on the server as .NET method or on the client as a client function. Both .NET method and client function must be parameterless.
The .NET method can be declared in any .NET class, including the source object itself. It can be static or instance method and must return List<CodeEffects.Rule.Common.DataSourceItem>:
using CodeEffects.Rule.Common;
...
public static List<DataSourceItem> GetPhysicians()
{
List<DataSourceItem> list = new List<DataSourceItem>();
list.Add(new DataSourceItem(1, "John Smith"));
list.Add(new DataSourceItem(2, "Anna Taylor"));
list.Add(new DataSourceItem(3, "Robert Brown"));
list.Add(new DataSourceItem(4, "Stephen Lee"));
list.Add(new DataSourceItem(5, "Joe Wilson"));
list.Add(new DataSourceItem(6, "Samuel Thompson"));
return list;
}
Obviously, I'm not populating the list from a database - just hard-coding those names for the sake of clarity.
The client functions that serve as menu sources must return an array of objects with two properties - "ID" and "Name":
function getPhysicians()
{
var list =
[
{ "ID": 1, "Name": "John Smith" },
{ "ID": 2, "Name": "Anna Taylor" },
{ "ID": 3, "Name": "Robert Brown" },
{ "ID": 4, "Name": "Stephen Lee" }
{ "ID": 5, "Name": "Joe Wilson" }
{ "ID": 6, "Name": "Samuel Thompson" }
];
return list;
};
Remember that JavaScript is case-sensitive. Therefore, the ID property must be all capital, the Name - in Pascal case.
So, methods such as these can be used as dynamic data sources. Now we need to tell the source object to use one of them for the PhysicianID property:
using CodeEffects.Rule.Attributes;
namespace CodeEffects.Rule.Demo.Asp.Common.Data
{
[Data("Physicians", typeof(Physician), "GetPhysicians")]
public class Patient
{
// C-tor
public Patient() { }
// This field uses the Physicians dynamic menu source
[Field(DisplayName = "Primary Care Physician", DataSourceName = "Physicians")]
public int PhysicianID { get; set; }
}
}
The CodeEffects.Rule.Attributes namespace now has a new DataAttribute class. Use this attribute on source object to declare your dynamic data sources. Because it's declared on class level, a single data source be used by multiple properties and fields of that class. The DataAttribute has three public constructors:
[Data("Physicians", "getPhysicians")]
This constructor takes only two parameters. The first is the name of the data source. We use this name to refer to this data source from the Field, Return or Parameter attributes. I'll show you how a bit later. The second parameter is the name of the function that returns the list of doctors. Because this constructor doesn't take any Type references, the DataAttribute assumes that this is a client function.
[Data("Physicians", typeof(Physician), "GetPhysicians")]
This constructor takes three parameters: the name, the type of the class that declares the data source method and the name of the method. Here, because we pass the type, the DataAttribute assumes that this is a "server" method.
[Data("Physicians", "CodeEffects.Rule.Demo.Asp",
"CodeEffects.Rule.Demo.Asp.Common.Data.Patient", "GetPhysicians")]
This constructor does the same as the previous one but instead of taking the type it takes type's assembly and full name.
I think it's clear now how it all works: the PhysicianID property (declared above in the source object) is decorated with the usual FieldAttribute. But now this attribute class has a new property called DataSourceName. New properties with this name are also added to the ReturnAttribute and ParameterAttribute class, making it possible to use Dynamic Menu Data Sources not only with rule fields, but also with returns of in-rule methods and parameters of actions and in-rule methods. Like this:
public string TestOne([Parameter(DataSourceName = "Physicians")] int physicianID)
{
return physicianID.ToString();
}
[return: Return(DataSourceName = "Physicians")]
public int TestTwo()
{
return 1;
}
In the Rule Editor the result looks like a static enum with doctor names as members:
There are several things to notice. Nothing serious, though:
- The Dynamic Menu Data Sources feature works only with properties and fields of integral types (int, uint, short, long, etc.) and their nullable equivalents. The value of DataSourceName property will be ignored on properties of all other types. If you think about it, you'll realize that making it work with other types doesn't make any practical sense. Well, with the exception of GUIDs, may be.
- You data source will be only "semi-dynamic" if you use the server-side .NET methods. In that case the list of menu items will be written in that big chunk of client data that Web Rule adds to your markup. If the list items get updated in the storage, the rule author will see the change only when (s)he refreshes the page (or, to put it more accurately, refreshes that chunk of client data.) But if you use client functions then the freshness of your data only depends on particular implementation of those functions because Web Rule calls them every time the rule author brings up the menu.
- You have to understand that Web Rule cannot possibly check if a particular value from the data source used to create the rule still exists in that source when it evaluates the rule. Therefore, it is your responsibility as developer to make sure that rules with non-existent values never get evaluated. The Evaluator class throws an exception on such rules.
- Fields and returns that have data sources use only "is equal to" and "is not equal to" operators. The nullable fields and returns add the "has no value" and "has any value" operators to that list. Again, if you think about it, there is no practical need for any other operators for such fields and returns.
- If the value of the DataSourceName property of the Field, Return or Parameter attributes is set, all other properties of these attribute classes except for Description and DisplayName will be ignored (no need for them.)
New Evaluator classes and new rule evaluation features. Answering the ever growing list of feature requests for the rule engine (and, quite honestly, we are pleasantly surprised by the size of that list - more please!!), Web Rule 3.0 includes several new ways to create and evaluate rules. Let me just mention the major points and then describe what it all means in details, with code samples:
- The Evaluator class now accepts descendant objects.
- At the time of writing this, removing or clearing the “type” attribute in <rule> node of rule XML tells the typed evaluator (that's the Evaluator<T> class) to bypass checking of the source's type while evaluating the rule against an instance of the source. We will definitely add some kind of a UseAnonimousSourceType property to the Asp.RuleEditor and Mvc.RuleEditor classes in the final version. For now, if you'd like to test this feature, simply remove the “type” attribute from the <rule> node before evaluating your rule.
- Added a new non-generic Evaluator class that takes a Type of source object as a parameter. Together with the first point, this helps if you create source objects on the fly and don't know their exact types at design time.
- Added a new DynamicEvaluator class that compiles a given rule (w/o type attribute present) for each source object’s type during evaluation. Keeping count of new Evaluator classes? Yep, there are 3 of them now - the old Evaluator<T>, the non-generic Evaluator and the new DynamicEvaluator. Don't worry, all of this will make perfect sense in just a bit :) All three use the same engine and your existing rule evaluation code won't brake.
- Properties with the same name but different types in base and descendant source objects no longer generate the “Ambiguous” exception.
- Added generic Filter extensions for IQueryable<> that allows pass-thru for LINQ providers, i.e. LINQ-to-SQL and Entity framework. This is huge! Details in a second.
- Added two new non-generic Filter extensions for IEnumerable and IQueryable. If using this type of filtering, you, the developer, take responsibility for typecasting at run-time.
So, this is what it all means, in details:
Imagine that we have one base class and two descendant (child) classes that inherit from the base class:
class ClassA
{
public int Id;
}
class ClassB : ClassA
{
public string Name { get; set; }
}
class ClassC : ClassA
{
public string Note { get; set; }
}
In version 3.0 we can use all three of them as source objects for the same rule - something that is not possible in version 2.0. Let's create three instances...
ClassA a = new ClassA { Id = 1 };
ClassB b = new ClassB { Id = 2, Name = "Test" };
ClassC c = new ClassC { Id = 3, Note = "Some Note" };
... and define a rule...
//Normally this rule would be created by the RuleEditor class
string rule =
@"<codeeffects xmlns='http://rule.codeeffects.com/schemas/rule'
xmlns:ui='http://rule.codeeffects.com/schemas/ui'>
<rule id='03b33dd0-4389-4ac4-a5aa-bd81fab41eb0' eval='true'
webrule='3.0.0.87' utc='5/10/2012 4:19:27 PM' type='{0}'>
<name>et35</name>
<definition>
<condition type='equal'>
<property name='Id' />
<value type='numeric'>2</value>
</condition>
</definition>
</rule>
</codeeffects>";
Notice the formatting of the "type" attribute. I'm doing this for the sake of this example - normally Web Rule takes care of all this stuff. Let's evaluate this rule against our instances:
using CodeEffects.Rule.Core;
...
// Here we are saying that this rule was creted for the ClassA class.
rule = string.Format(
rule, System.Web.HttpUtility.HtmlEncode(typeof(ClassA).AssemblyQualifiedName));
Evaluator<ClassA> ev = new Evaluator<ClassA>(rule);
Console.WriteLine(ev.Evaluate(a)); // Produces false
Console.WriteLine(ev.Evaluate(b)); // Produces true
Console.WriteLine(ev.Evaluate(c)); // Produces false
Because the rule only refers to one property "Id" and all three classes declare or inherit this property, the rule evaluation doesn't fail in version 3.0. This demonstrates the point # 1.
Because we set the value of the "type" attribute in the code above to the base type ClassA, if you were to change that value to ClassB, you’d get an exception in a constructor: “Unhandled Exception: System.Exception: The given ruleset does not contain any rules with type ClassA”. This is to prevent run-time errors. By setting the type attribute to “ClassB” we are saying that it should only be compiled by evaluators of type ClassB or its descendants.
By clearing the type attribute, we can tell Web Rule that evaluator should try to compile it against a given type. So, the following will execute without errors:
rule = string.Format(rule, "");
ev = new Evaluator<ClassA>(rule);
Console.WriteLine(ev.Evaluate(a)); // Produces false
Console.WriteLine(ev.Evaluate(b)); // Produces true
Console.WriteLine(ev.Evaluate(c)); // Produces false
This demonstrates the point # 2. Again, we plan to add a property to RuleEditor classes that would control the value of the "type" attribute in the final release.
The strongly-typed generic evaluator requires you to know all types upfront. However, in cases when you generate assemblies and types dynamically (Emit, CodeDom, Expressions, etc.), you may not know objects’ types until run-time. In this case use the new Evaluator which accepts type as a parameter in a constructor. This evaluator is slower than the generic one, but only a little bit.
//The rule was created for the ClassA
rule = string.Format(
rule, System.Web.HttpUtility.HtmlEncode(typeof(ClassA).AssemblyQualifiedName));
//The evaluator receives its type from a descendant of the ClassA: ClassB
Evaluator e = new Evaluator(b.GetType(), rule);
Console.WriteLine(e.Evaluate(b)); //true
This code details the point # 3 from above.
Alternatively, if performance is not critical and you do not want to or cannot deal with types at design-time, we’ve introduced a completely new evaluator class - the DynamicEvaluator. It works by creating typed evaluators internally for each new type encountered during rule evaluation. Beware however, since compilation occurs during evaluation, if an object does not have or inherit members referenced in the rule, it will throw an “Unhandled Exception: System.ArgumentException: 'X' is not a member of type 'ClassA'” exception. The performance hit is not significant, though, unless you have millions of instances of source objects to evaluate. We are still talking about milliseconds.
// As before, use the type-agnostic rule XML
rule = string.Format(rule, String.Empty);
DynamicEvaluator de = new DynamicEvaluator(rule);
Console.WriteLine(de.Evaluate(a)); // Produces false
Console.WriteLine(de.Evaluate(b)); // Produces true
Console.WriteLine(de.Evaluate(c)); // Produces false
The above code completes the point # 4.
In rare cases when you have two child classes hiding base properties, you would get an “Ambiguous” exception, stating that duplicate matches were found. For the exception to occur, two properties must have the same name, but different return types. This happens because Type’s GetMethod and GetProperty methods flatten hierarchy, ignore case, and ignore the types of properties. Specifically, property getters are methods and method signature does not include a return type.
In version 3.0 we now catch this exception and use the first child’s property or field matching given name. So, the following will work in 3.0:
class ClassX
{
public int Id;
}
class ClassY : ClassX
{
public string Id { get; set; }
public string Name { get; set; }
}
...
ClassX x = new ClassX { Id = 1 };
ClassY y = new ClassY { Id = "2", Name = "Test" };
// this rule uses the Id field as a numeric
rule =
@"<codeeffects xmlns='http://rule.codeeffects.com/schemas/rule'
xmlns:ui='http://rule.codeeffects.com/schemas/ui'>
<rule id='03b33dd0-4389-4ac4-a5aa-bd81fab41eb0' eval='true'
webrule='3.0.0.87' utc='5/10/2012 4:19:27 PM' type=''>
<name>Test rule</name>
<definition>
<condition type='equal'>
<property name='Id' />
<value type='numeric'>1</value>
</condition>
</definition>
</rule>
</codeeffects>";
Evaluator evXY = new Evaluator(x.GetType(), rule);
Console.WriteLine(evXY.Evaluate(x)); // Produces true
Console.WriteLine(evXY.Evaluate(y)); // Produces false
// This rule references the Id field of string type
rule =
@"<codeeffects xmlns='http://rule.codeeffects.com/schemas/rule'
xmlns:ui='http://rule.codeeffects.com/schemas/ui'>
<rule id='03b33dd0-4389-4ac4-a5aa-bd81fab41eb0' eval='true'
webrule='3.0.0.87' utc='5/10/2012 4:19:27 PM' type=''>
<name>Test rule</name>
<definition>
<condition type='equal'>
<property name='Id' />
<value type='string'>2</value>
</condition>
</definition>
</rule>
</codeeffects>";
evXY = new Evaluator(y.GetType(), rule);
Console.WriteLine(evXY.Evaluate(y)); // Produces true
This was point # 5.
We now support IQueryable<>. How cool is that!
First, let’s take a look at the IEnumerable<> Filter extension:
rule =
@"<codeeffects xmlns='http://rule.codeeffects.com/schemas/rule'
xmlns:ui='http://rule.codeeffects.com/schemas/rule/ui'>
<rule id='03b33dd0-4389-4ac4-a5aa-bd81fab41eb0' eval='true'
webrule='3.0.0.87' utc='5/10/2012 4:19:27 PM' type='{0}'>
<name>Test rule</name>
<definition>
<condition type='greater'>
<property name='Id' />
<value type='numeric'>1</value>
</condition>
</definition>
</rule>
</codeeffects>";
rule = string.Format(
rule, System.Web.HttpUtility.HtmlEncode(typeof(ClassA).AssemblyQualifiedName));
//rule = string.Format(rule, String.Empty);
ClassA[] array = new[]
{
new ClassA { Id = 1 },
new ClassB { Id = 2, Name = "Test" },
new ClassB { Id = 3 }
};
IEnumerable<ClassA> result = array.Filter(rule);
foreach(var item in result)
{
Console.WriteLine(item.Id);
}
// Produces:
// 2
// 3
Results are returned as another IEnumerable<>. This works well and very efficiently. By contract, the IQueryable<> extension returns an expression, which allows for evaluation and delayed execution:
var result3 = array.AsQueryable<ClassA>().Filter(rule);
Console.WriteLine(result3.ToString());
foreach(var item in result3)
{
Console.WriteLine(item.Id);
}
// Produces following output
// YourApplication.ClassA[].Where(x => (x.Id > 1))
// 2
// 3
In this example the result3 variable is of type IQueryable<ClassA> and holds a Where clause with a lambda expression (x => x.Id > 1). This expression is available for further manipulation and gets compiled and executed in the foreach iterator (delayed execution).
However, the true advantage shines when applied to LINQ-to-SQL objects. The LINQ-to-SQL provider converts expressions into SQL statements and executes them on the database server. So, instead of retrieving all data first and filtering resulting set in memory, it now converts the rule into SQL, runs it on the server, and returns already filtered result set.
Consider following example, using AdventureWorks database:
AdventureWorksDataContext db = new AdventureWorksDataContext();
db.Log = Console.Out;
string dbrule =
@"<codeeffects xmlns='http://rule.codeeffects.com/schemas/rule'
xmlns:ui='http://rule.codeeffects.com/schemas/rule/ui'>
<rule id='03b33dd0-4389-4ac4-a5aa-bd81fab41eb0' eval='true'
webrule='3.0.0.87' utc='5/10/2012 4:19:27 PM' type=''>
<name>Test rule</name>
<definition>
<method name='StartsWith' instance='true'>
<property name='Name' />
<value type='string'>A</value>
</method>
</definition>
</rule>
</codeeffects>";
var products = db.Products.Filter(dbrule);
foreach(var product in products)
{
Console.WriteLine("{0,3}: {1}", product.ProductID, product.Name);
}
/* produces the following ouput
SELECT [t0].[ProductID], [t0].[Name], ...
FROM [Production].[Product] AS [t0]
WHERE [t0].[Name] LIKE @p0
-- @p0: Input NVarChar (Size = 4000; Prec = 0; Scale = 0) [A%]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 4.0.30319.1
1: Adjustable Race
879: All-Purpose Bike Stand
712: AWC Logo Cap
*/
It's even smart enough to create joins where necessary:
dbrule =
@"<codeeffects xmlns='http://rule.codeeffects.com/schemas/rule'
xmlns:ui='http://rule.codeeffects.com/schemas/rule/ui'>
<rule id='03b33dd0-4389-4ac4-a5aa-bd81fab41eb0' eval='true'
webrule='2.0.0.8' utc='10/20/2011 4:19:27 PM' type=''>
<name>et35</name>
<definition>
<method name='StartsWith' instance='true'>
<property name='ProductModel.Name' />
<value type='string'>A</value>
</method>
</definition>
</rule>
</codeeffects>";
var products2 = db.Products.Filter(dbrule);
foreach(var product in products2)
{
Console.WriteLine("{0,3}: {1}", product.ProductID, product.Name);
}
/* produces the following ouput:
SELECT [t0].[ProductID], [t0].[Name], ...
FROM [Production].[Product] AS [t0]
LEFT OUTER JOIN [Production].[ProductModel] AS [t1] ON [t1].[ProductModelID] = [t0].[ProductModelID]
WHERE [t1].[Name] LIKE @p0
-- @p0: Input NVarChar (Size = 4000; Prec = 0; Scale = 0) [A%]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 4.0.30319.1
879: All-Purpose Bike Stand*/
Note, however, that if we naively decided to include product model’s name in the output, it would generate an extra select statement to retrieve missing information:
foreach(var product in products2)
{
Console.WriteLine("{0,3}: {1} ({2})",
product.ProductID, product.Name, product.ProductModel.Name);
}
/* produces following ouput
SELECT [t0].[ProductID], [t0].[Name], ...
FROM [Production].[Product] AS [t0]
LEFT OUTER JOIN [Production].[ProductModel] AS [t1] ON [t1].[ProductModelID] = [t0].[ProductModelID]
WHERE [t1].[Name] LIKE @p0
-- @p0: Input NVarChar (Size = 4000; Prec = 0; Scale = 0) [A%]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 4.0.30319.1
SELECT [t0].[ProductModelID], [t0].[Name], [t0].[CatalogDescription], [t0].[Instructions],
[t0].[rowguid], [t0].[ModifiedDate]
FROM [Production].[ProductModel] AS [t0]
WHERE [t0].[ProductModelID] = @p0
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [122]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 4.0.30319.1
879: All-Purpose Bike Stand (All-Purpose Bike Stand)
*/
This is because we are filtering and iterating over Products that do not include joined properties, such as ProductModel. Obviously, having select statement for each product record is very inefficient. One way of solving this problem is to include necessary field in the query. This is, by the way, where the new dynamic evaluator comes in handy:
var products3 = from product in db.Products
select new { product.ProductID, product.Name, product.ProductModel };
foreach(var product in products3.Filter(dbrule))
{
Console.WriteLine("{0,3}: {1} ({2})", product.ProductID, product.Name, product.ProductModel.Name);
}
/* produces the following ouput:
SELECT [t0].[ProductID], [t0].[Name], [t2].[test], [t2].[ProductModelID], [t2].[Name] AS [Name2],
[t2].[CatalogDescription], [t2].[Instructions], [t2].[rowguid], [t2].[ModifiedDate
]
FROM [Production].[Product] AS [t0]
LEFT OUTER JOIN (
SELECT 1 AS [test], [t1].[ProductModelID], [t1].[Name], [t1].[CatalogDescription],
[t1].[Instructions], [t1].[rowguid], [t1].[ModifiedDate]
FROM [Production].[ProductModel] AS [t1]
) AS [t2] ON [t2].[ProductModelID] = [t0].[ProductModelID]
WHERE [t2].[Name] LIKE @p0
-- @p0: Input NVarChar (Size = 4000; Prec = 0; Scale = 0) [A%]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 4.0.30319.1
879: All-Purpose Bike Stand (All-Purpose Bike Stand)
*/
This is better. But now the query looks somewhat convoluted. That’s due to nullable join to ProductModel as a record. We can further optimize this by selecting field names specifically:
var products4 = from product in db.Products
select new { product.ProductID, product.Name, ModelName = product.ProductModel.Name };
foreach(var product in products4.Filter(dbrule))
{
Console.WriteLine("{0,3}: {1} ({2})", product.ProductID, product.Name, product.ModelName);
}
Unfortunately, this will produce an exception because the rule evaluates a collection of anonymous types that do not have the ProductModel property. Outside of modifying the rule itself by replacing <property name=’ProductModel.Name’> with <property name=’ModelName’> (which we could do), the easier way is to simply apply the filter to the db.Products collection and then run a query on it. This is possible since the Filter extension returns an expression which does not get evaluated until later (foreach, Count(), ToArray(), etc.). So following will work nicely:
var products5 = from product in db.Products.Filter(dbrule)
select new { product.ProductID, product.Name, ModelName = product.ProductModel.Name };
foreach(var product in products5)
{
Console.WriteLine("{0,3}: {1} ({2})", product.ProductID, product.Name, product.ModelName);
}
/* produces the following ouput:
SELECT [t0].[ProductID], [t0].[Name], [t1].[Name] AS [ModelName]
FROM [Production].[Product] AS [t0]
LEFT OUTER JOIN [Production].[ProductModel] AS [t1] ON [t1].[ProductModelID] = [t0].[ProductModelID]
WHERE [t1].[Name] LIKE @p0
-- @p0: Input NVarChar (Size = 4000; Prec = 0; Scale = 0) [A%]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 4.0.30319.1
879: All-Purpose Bike Stand (All-Purpose Bike Stand)
*/
Obviously, there are restriction on types of operations that can be performed in this manner. Most are limitations of Linq-to-SQL provider, especially around case sensitivity issues. We are working to make it as robust as possible. On the other hand, IQueryable<> support allows us to apply rules to other back-end systems. Imagine what kind of amazing advanced search forms or reporting grids you can create using this functionality of Web Rule 3.0!
So, all this was to showcase the point # 6 from above. And the last one is related to the new non-generic Filter extensions.
Version 3.0 supports IEnumerable and IQueryable for dynamically generated types. This is helpful when the source type is not known at compile time. The drawback of this approach is that you are responsible for type-casting results since these interfaces work with objects only.
Returning to our earlier example of IEnumerable<> and using corresponding rule, this illustrates how to use IEnumerable:
System.Collections.IEnumerable result2 = array.Filter(typeof(ClassA), rule);
foreach(var item in result2)
{
Console.WriteLine((item as ClassA).Id);
}
// Produces:
// 2
// 3