Monday, April 05, 2010
« CSLA 4 data portal changes | Main | CSLA 4 business rule chaining »

The first preview of the new CSLA 4 business rules subsystem will be available soon from http://www.lhotka.net/cslanet/download.aspx.

Of course there are a lot of other changes to CSLA .NET in this preview, so make sure to carefully read the change log. Although there are a lot of breaking changes, most of them have pretty minimal impact on people who are already using the 3.8 coding style for classes. Except for the business rules – that impacts everyone.

This is a major change to the way business and validation rules work, with some pretty amazing new capabilities as a result.

The next preview will roll authorization rules into this as well, and that’ll be the last major change for CSLA 4.

The business rule changes apply to both Silverlight and .NET – as always the idea is that the vast majority of your business code should be the same regardless of platform.

I’d like to summarize the primary changes from 3.8 to 4 in regards to business and validation rules.

Simple changes

The simplest change is that the ValidationRules property in BusinessBase is now named BusinessRules. Also, the Csla.Validation namespace has been replaced by the Csla.Rules namespace.

If you are using DataAnnotations validation attributes in 3.8, they continue to work in 4 without change.

That was the easy part. Now for the interesting changes.

Rule classes

Rule methods are replaced by rule classes. This means a 3.8 rule like this:

private static bool MyRule<T>(T target, RuleArgs e) where T : Customer
{
  if (target.Sales < 10)
  {
    e.Description = "Customer has low sales";
    e.Severity = RuleSeverities.Information;
    return false;
  }
  else
    return true;
}

becomes a class like this:

private class MyRule : Csla.Rules.BusinessRule
{
  protected override void Execute(RuleContext context)
  {
    var target = (Customer)context.Target;
    if (target.Sales < 10)
      context.AddInformationResult("Customer has low sales");
  }
}

It is the same basic rule, but packaged just a little differently. Rule types must implement IBusinessRule, but it is easier to inherit from BusinessRule, which provides a set of basic functionality you’ll typically need when implementing a rule.

The most important thing to understand is that the RuleContext parameter provides input and output for the Execute() method. The context parameter includes a bunch of information necessary to implement the rule, and has methods like AddErrorResult() that you use to indicate the results of the rule.

There’s one coding convention that you must follow: the protected properties from BusinessRule must never be changed in Execute(). I wish .NET had the ability to create an immutable type – a class where you could initialize properties as the object is created, and then ensure they are never changed after that point. But such a concept doesn’t exist, at least in C# or VB. But that is what you must do with rule objects. You can set the properties of the rule object as it is created. After that point, if you change properties of the rule object you are going to cause bugs. So do not change the properties of a rule in Execute() and you’ll be happy.

AddRule Method

In the Customer business class you still override AddBusinessRules(), and that code looks like this:

protected override void AddBusinessRules()
{
  base.AddBusinessRules();
  BusinessRules.AddRule(new MyRule { PrimaryProperty = SalesProperty });
}

The new AddRule() method accepts an instance of an IBusinessRule instead of a delegate like in 3.8. This one instance of the rule is used for all instances of the business object type. In other words, exactly one instance of MyRule is created, and it is used by all instances of Customer. You must be aware of this when creating a rule class, because it means you can never alter instance-level fields or properties after the rule is initialized. If you do alter an instance-level field or property, you’ll affect the rule’s behavior for all Customer objects in the entire application – and that’d probably be a very bad thing.

Most rule methods that require a primary property will actually require it on the constructor. For example, look at the Required and MaxLength rules:

protected override void AddBusinessRules()
{
  base.AddBusinessRules();
  BusinessRules.AddRule(new MyRule { PrimaryProperty = SalesProperty });
  BusinessRules.AddRule(new Csla.Rules.CommonRules.Required(NameProperty));
  BusinessRules.AddRule(new Csla.Rules.CommonRules.MaxLength(NameProperty, 20));
}

The result is the same either way, but when you are writing your rules it is typically best to explicitly require a primary property on the constructor if you plan to use the primary property value. Later I’ll talk about per-object rules that have no primary property.

You should also notice that the properties are always Csla.Core.IPropertyInfo, not string property names. The new business rule system requires that you use PropertyInfo fields to provide metadata about your properties. This syntax has been available since CSLA 3.5, and for my part I haven’t created a business class without them for years. This is one part of a larger effort to eliminate the use of string property names throughout CSLA and CSLA-based business code.

Invoking Rules

In the previous AddRule() call, the PrimaryProperty property of the rule is set to SalesProperty. This is what links the rule to a specific property. So when the Sales property changes, this rule will be automatically invoked. As in 3.8, all rules are invoked (by default) when an object is created through the data portal, and can be invoked by calling BusinessRules.CheckRules(). And you can invoke rules for a property with BusinessRules.CheckRules(SalesProperty) – though you should notice that CheckRules() now requires an IPropertyInfo, not a string property name.

If you don’t provide a PrimaryProperty value as shown in this example, the rule method is associated with the business type, not a specific property. This means that the rule will run when CheckRules() is called with no parameter, or when the new CheckObjectRules() method is called to invoke rules attached to the business type.

In summary, there are now three CheckRules() overloads:

  1. CheckRules() – checks all rules for an object
  2. CheckObjectRules() – checks all rules not associated with a specific property (object rules)
  3. CheckRules(property) – checks all rules for a specific property

The same concepts of priority and short-circuiting from 3.8 apply in 4.

InputProperties and InputPropertyValues

You can write that same rule just a little differently, making it unaware of the Target object reference:

public class MyRule : Csla.Rules.BusinessRule
{
  public MyRule(Csla.Core.IPropertyInfo primaryProperty)
    : base(primaryProperty)
  {
    InputProperties = new List<Csla.Core.IPropertyInfo> { primaryProperty };
  }

  protected override void Execute(RuleContext context)
  {
    var sales = (double)context.InputPropertyValues[PrimaryProperty];
    if (sales < 10)
      context.AddInformationResult("Sales are low");
  }
}

This is a little more complex and a lot more flexible. Notice that the rule now requires a primary property, which is actually managed by the BusinessRule base class. Also notice that it adds the primaryProperty value to a list of InputProperties. This tells CSLA that any time this rule is invoked, it must be provided with that property value.

In the Execute() method notice how the sales value is retrieved from the context by using the InputPropertyValues dictionary. This dictionary contains the values from the business object based on the properties in the InputProperties list. A rule can require many property values be provided. Of course the rule will only work with objects that actually have those properties!

Because this rule will work with any object that can provide a double property value, the class is now public and could be placed in a library of rules.

The AddRule() method call in Customer must change a little:

protected override void AddBusinessRules()
{
  base.AddBusinessRules();
  BusinessRules.AddRule(new MyRule(SalesProperty));
}

As before, the PrimaryProperty property is being set, but this time it is through the constructor instead of setting the property directly. This is because the rule now requires the primary property value, and can only work if it is supplied.

Business Rules: Rules that modify data

The CSLA 4 rules system formally supports business rules. Really 3.8 did too, but it was a little confusing with the terminology, which is why ValidationRules is now called BusinessRules – to avoid that confusion.

The RuleContext object now has an AddOutValue() method you can use to provide an output value from your rule:

context.AddOutValue(NameProperty, “Rocky”);

When the rule completes, any out values are updated into the business object using the LoadProperty() method (so there’s no circular loop triggered).

This AddOutValue() technique is safe for synchronous and asynchronous rules, because the business object isn’t updated until the rule completes and control has returned to the UI thread.

If you are creating a synchronous rule, the RuleContext does have a Target property that provides you with a reference to the business object, so you could set the property yourself – just remember that setting a property typically triggers validation, which could cause an endless loop. Overall, it is best to use AddOutValue() to alter values.

Validation Rules: Errors, Warnings and Information

As in 3.8, CSLA 4 allows validation rules to provide an error, a warning or an informational message. The RuleContext object has three methods:

  1. AddErrorResult()
  2. AddWarningResult()
  3. AddInformationalResult()

Each of these methods takes a string parameter which is the human-readable text to be displayed. A rule should only call one of these methods. If the rule calls them more than once, only the last one will have effect.

If AddErrorResult() is called, the rule is returning a failure result, and the business object will become invalid (IsSelfValid will be false).

If no method is called, or AddWarningResult() or AddInformationalResult() is called the rule is returning a success result and the business object will not become invalid. This is basically the same as the 3.8 behavior.

Dependent Properties

The concept of dependent properties is quite different in the new system. Instead of an AddDependentProperty() method, a rule now indicates the properties it affects. This can be coded into the rule itself or specified when AddRule() is called. Either way, what’s happening is that every rule object maintains a list of AffectedProperties for that rule.

When a rule completes a PropertyChanged event is raised for every property in AffectedProperties, as well as any properties altered through AddOutValue().

It is also important to realize that, with an async rule, all properties in AffectedProperties will be marked busy when the rule starts, which typically means the user will see a busy animation (if you are using the PropertyStatus control) to show that the property has some outstanding operation running.

Rule Sets

This is a major new feature of the rules system that is designed to support web applications where multiple customers/clients/organizations are sharing the same virtual root and application. In that case it might be necessary to have different business rules for each customer/client/organization. Rule sets are a way to accomplish this goal.

When you call BusinessRules.AddRule(), the rules are added to a rule set. Normally they are added to the default rule set, and if you only need one set of rules per type of object you can just be happy – it works. But if you need multiple sets of rules per type of object (such as one set of rules for customer A and a different set for customer B), then you’ll want to load each set of rules and attach those rules to your business object type.

This is done by changing the BusinessRules.RuleSet property. For example:

protected override void AddBusinessRules()
{
  base.AddBusinessRules();

  // add rules to default rule set
  BusinessRules.AddRule(...);
  BusinessRules.AddRule(...);
  BusinessRules.AddRule(...);

  // add rules to CustA rule set
  BusinessRules.RuleSet = "CustA";
  BusinessRules.AddRule(...);
  BusinessRules.AddRule(...);
  BusinessRules.AddRule(...);

  // add rules to CustB rule set
  BusinessRules.RuleSet = "CustB";
  BusinessRules.AddRule(...);
  BusinessRules.AddRule(...);
  BusinessRules.AddRule(...);

  // use default rule set
  BusinessRules.RuleSet = “default”;
}

This code loads rules into three rule sets: default, CustA and CustB. It is up to you to set the RuleSet property to the correct value for each business object instance.

To set the rule set, set the BusinessRules.RuleSet property, then make sure to call BusinessRules.CheckRules() to apply the new set of rules.

The RuleSet value is serialized along with the state of the business object, so you can (and typically will) set the RuleSet property in your DataPortal_Create() and DataPortal_Fetch() methods, and that rule set will be used through the life of the object. I expect the most common scenario will be to set the RuleSet based on the identity of the current user, or some context value in Csla.ApplicationContext.ClientContext.

Per-Object Rules

Finally, it is possible to have rules that are not attached to a specific primary property. These rules are attached to the business object. For example:

protected override void AddBusinessRules()
{
  base.AddBusinessRules();
  BusinessRules.AddRule(new MyObjectRule());
}

private class MyObjectRule
{
  protected override void Execute(RuleContext context)
  {
    var target = (Customer)context.Target;
    // check rules here
  }
}

Notice that the AddRule() method has no primary property specified. Because there’s no primary property, the rule is attached to the business object itself. Normally this type of rule is a private rule in the business class, and uses the business object’s properties directly. But you can specify input and affected properties, as well as provide output values as discussed earlier.

Per-object rules are run in two cases:

  • You call BusinessRules.CheckRules()
  • You call BusinessRules.CheckObjectRules()

Per-object rules are not run automatically when properties change. So if you don’t invoke them, they won’t run.

Async Rules

One of my primary goals in designing the new rule system is to provide a lot of consistency between sync and async rules. To this end, both sync and async rules are constructed the same way: by implementing IBusinessRule or subclassing BusinessRule.

But there are some extra restrictions on async rules. Most notably, async rules must get their input values through the RuleContext, and must provide any output values through RuleContext. To help enforce this, the context.Target property is always null in an async rule. This should help prevent the rule from trying to interact directly with the business object.

The reason this is so important, is that I assume an async rule will run some of its code on a background thread. Most of CSLA (and .NET) is not threadsafe, so having multiple threads interact with a business object will cause problems. If the async rule uses the RuleContext as a message to get and return values, CSLA can help ensure processing occurs on the correct thread.

Here’s a simple async rule. It is a little silly, in that all it does is ToUpper() a string value, but it should give you the idea:

public class AsyncToUpper : Csla.Rules.BusinessRule
{
  public AsyncToUpper(Csla.Core.IPropertyInfo primaryProperty)
    : base(primaryProperty)
  {
    IsAsync = true;
    InputProperties = new List<Csla.Core.IPropertyInfo> { primaryProperty };
  }

  protected override void Execute(Csla.Rules.RuleContext context)
  {
    var bw = new System.ComponentModel.BackgroundWorker();
    bw.DoWork += (o, e) =>
    {
      var value = (string)context.InputPropertyValues[PrimaryProperty];
      context.AddOutValue(PrimaryProperty, value.ToUpper());
    };
    bw.RunWorkerCompleted += (o, e) =>
    {
      if (e.Error != null)
        context.AddErrorResult(e.Error.Message);
      context.Complete();
    };
    bw.RunWorkerAsync();
  }
}

The rule indicates that it is async by setting IsAsync to true in its constructor. This tells CSLA that the rule expects to run some or all of its logic on a background thread, so CSLA does some extra work for you. Specifically, CSLA marks the primary property and any AffectedProperties as busy before the rule starts and not busy when it ends. It also sets up the rule invocation so its results are handled through an async callback within CSLA. And it makes sure the RuleContext.Target property is null.

Next, notice that the InputProperties list is initialized in the rule’s constructor. This is the list of property values the rule requires to operate, and these property values will be provided through the RuleContext to the Execute() method in the InputPropertyValues dictionary.

The Execute() method itself is using a BackgroundWorker to run some code on a background thread. You can use BackgroundWorker or the async DataPortal and they’ll work fine. The one big requirement here is that whatever you use must ensure that the competed callback is on the UI thread. It is your responsibility to make sure the completed callback is on the UI thread (if any). The BackgroundWorker and data portal do this for you automatically. If you use some other async technology you must take steps to make sure this is done correctly.

The AddOutValue() method is used to provide the output value. Remember that the actual business object property isn’t affected until after the rule completes, which is when CSLA is handling the results of your rule on the UI thread (where it can safely update the business object).

The RunWorkerCompleted event is used to handle any async errors. You’d do the same thing with the data portal, handling the FetchCompleted event (or similar). It is important to remember that exceptions occurring on a background thread are lost unless you carry them through. The code shown here is following what I typically do, which is to add an error result from the rule in the case of an async exception.

One last thing to keep in mind: there is exactly one instance of your rule object being used by all instances of a business type. Because of this, the Execute() method must be atomic and stateless. To put it another way, you should never, ever, ever alter any instance-level fields or properties of the rule object from the Execute() method. If you do alter an instance-level field or property of the rule object, that change will affect all business objects, not just the one you are running against right now. And with async rules you’ll run into race conditions and other nasty multi-threading issues. This is really the same restriction I mentioned earlier with sync rules – don’t change rule properties in Execute() – but it is so important that I wanted to reiterate the point here too.

While there are some restrictions on how you construct an async rule, I am pretty happy with how similar sync and async rules are to implement. In fact, all the async concepts (input values, AddOutValue()) work just fine with sync rules too.

Moving from 3.8 to 4

While the new business rules system is somewhat different from the 3.8 implementation, the process of moving from 3.8 to 4 isn’t terribly painful.

  • Every rule method must become a rule class. This is a pretty mechanical process, but obviously does require some work
  • Every use of ValidationRules must be changed to BusinessRules
  • Every AddRule() call will be affected, which is another mechanical change
  • Dependent properties become AffectedProperties on each AddRule() method call

I was able to get the entire unit test codebase changed over in less than 8 hours – and that included changes not only for the business rules, but also for the data portal and several other breaking changes. I don’t mean to trivialize the effort required, but the changes are mostly mechanical in nature, so it is really a matter of plowing through the code to make the changes, which is mostly repetitive and boring, not really hard.

Summary

I’m using the major version change from 3 to 4 as an opportunity to make some fairly large changes to CSLA. Changes that enable some key scenarios needed by Magenic customers, and requested by people on the forum explicitly or implicitly. Some effort will be required to upgrade, but I suspect most people will find it well worth the time.

The big changes are:

  • Rule sets
  • Per-object rules
  • Rule objects instead of rule methods
  • Common model for sync and async rules

I’m pretty excited about these changes, and I hope you find them useful!

Monday, April 05, 2010 6:51:21 PM (Central Standard Time, UTC-06:00)
Rocky,

Some interesting changes there.
My initial reactions are mixed though:

1. Removing delegates as business rules seems like a bad idea off the top of my head.
Firstly I don't like class bloat, and with CSLA 4 it looks like there are going to be a lot more classes added. I liked having static Rules classes which had a collection of common rules methods.
Secondly, the ability to define rules in line in the business object through something like a lambda makes for more readable code imo.


2. The ruleset idea is a non-starter in that it won't meet the stated goal for an ISV like my company.
Sure if you know all your clients at the start of development - and the client list is not going to change - then rulesets can work.
But what about a real world, dynamic environment where I want to host multiple clients through my application and the client list is constantly changing?
Everytime the client list changes I would have to change code given the hardcoded nature of the ruleset solution.
Truth is that I can already accomplish the stated goal in 3.8 already thanks to delegates. I add generic validation rules to properties and then within the rules methods I have logic to check the application context and determine whether rules apply.
So not really sure what rulesets really bring to the table beyond neater organisation of client-specific rule logic.


3. If rulesets are going to your solution to authorisation rules, then I'm bummed.
I have posted several times on the forums about the issue with authorisation rules - that being their static nature and use of simple profile string names.
Authorisation rules must be able to support dynamic situations where the application context - i.e. user properties, application state and object instance data - can all be used to determine an authorisation outcome. Hardcoded rulesets will not cut it.


4. Per object rules are a much needed addition and look good.


Of course my commentary is from a specific view but hope you find it constructive.
Paul
Monday, April 05, 2010 9:02:21 PM (Central Standard Time, UTC-06:00)
I have to say I totally agree with Paul here, especially on the authorization.
Dennis
Monday, April 05, 2010 9:13:24 PM (Central Standard Time, UTC-06:00)
I really like the per object rules. I assume that means we can eliminate the use of the "fake property" trick now.

I noticed in one of your posts that you recommended not calling
CheckRules() in an override of IsValid. Where do you recommend we call CheckObjectRules then?

Joe
Joe Fallon
Monday, April 05, 2010 10:17:50 PM (Central Standard Time, UTC-06:00)
Paul,

Thanks for the feedback.

1) The use of classes instead of delegates was debated at some length on the forum a few months ago. The value of using an interface-based class model is very high, and the overhead is essentially identical to using delegates if not lower. Certainly performance should be marginally better because calling a method directly is a tiny bit faster than invoking a delegate.

The big thing is that rules need two types of metastate. They need metastate that defines the rule itself, such as the Max value in a MaxValue rule. And they need metastate that is per-call, such as the current state of the business object at the time of rule evaluation. If rules are limited to being a method, then both kinds of metastate must be externalized somewhere else.

In the 2.0-3.8 model the static metastate (like the Max value) is externalized into a RuleArgs object that you had to subclass. So you had to create the rule method and a custom RuleArgs subclass. Then the contextual metastate was haphazardly provided through the RuleArgs parameter, but not really. And there was no way to do a return value beyond the bool result of the method.

Not that it was a terrible model - it has worked for a lot of people for many years. But its weaknesses really became apparent when we had to introduce async rules for Silverlight. And when people want a rule to return multiple results, or they want to invoke external rule engines or workflows. Those things just aren't possible with the old model.

Now I could have kept the method-based scheme, and expanded the RuleArgs concept and added the RuleContext concept. But you'd still be stuck in this situation where creating a rule means creating the rule method and defining a custom RuleArgs subclass.

Make the rule become an object consolidates the rule method and the static metastate container into one thing, which actually means you have to write less code and you have less concepts to deal with.

And it clarifies the difference between static and context state, and consolidates the sync and async rule models into one model.

Oh, and it enables a rule to return multiple results, and even invoke other rules (I forgot to put rule chaining in the blog post - need to add that), and invoke external rule engines or workflows.

2) There are two answers to the dynamic rule requirement.

One is rulesets, which aren't nearly as static as you seem to think. In fact, now that rules are just classes, they are easily defined by a full type name and you don't need fancy reflection techniques to create rules based on metadata. In fact, you should be able to use MEF to dynamically load rules for a business object type (though I can't say that I've walked through that, so I'm not 100% sure). Still, if you have a metadata model that describes the rules for your customers you can load the rules dynamically into rulesets.

A second is to create your own dynamic rule engine. Again, I forgot to cover rule chaining in the blog post and will have to fix that. But rule chaining means that you can create a rule that invokes other rules. So if you don't find my ruleset scheme to be powerful enough, no problem. Just create a rule that loads and invokes the right rules for your requirement.

Basically I've opened up the ultimate escape hatch, allowing you to write a "rule" that is actually a complete rule subsystem of your own.

3) I didn't even talk about authorization rules in the post besides suggesting that they'll be conceptually rolled into the business rule system. I think you are reading too much (or too little) into the changes I'll be making. I'm not sure how you can read anything at all, since I didn't really say anything at all…

And I'm not going to give a full rundown in this reply, as it requires a big blog post by itself. But consider that authz rules are rules - they are a class that inherits from AuthorizationRule, much like a business rule inherits from BusinessRule. The default rules provided with CSLA will be role-based just like today. But you'll be able to create your own rules that act against the object and any other environmental context you can access. So for per-property rules (where you have an actual business object instance in RuleContext.Target) you should be able to get very fancy indeed.

For per-type rules things are still harder, because there is no business object instance yet (for create/fetch/delete). So creating a rule that operates on business object state is clearly impossible, because there is no business object. In the case of insert/update/deleteself there is a business object, and those rules will be not unlike object-level business rules.

If you have ideas how the create/fetch/delete rules can be made more sophisticated I'm all ears. But I'm really unable to see how you'll be able to do anything beyond operate against roles and ambient context (stuff in ApplicationContext, etc), because there just isn't a business object yet, so there can't be any business object-specific context to use.

4) Thanks, I think they'll solve a common problem.
Monday, April 05, 2010 10:22:05 PM (Central Standard Time, UTC-06:00)
Joe,

The idea behind the rules system is that rules are triggered based on changes to the object state. IsValid should be a passive property that is used to find out if the object graph is valid, and it is usually called in response to the object's state changing (including a rule becoming broken or unbroken).

So invoking rules in IsValid may not cause an infinite loop, but it is quite likely to cause your rules to be invoked many more times than they should be. If you have any rules that are expensive (like ones that talk to the server) this could easily become a problem.

What you need to do is decide what circumstance should trigger running your per-object rules. Is it when a property of your object changes (override OnPropertyChanged)? Is it when a property of any object in your object graph changes (override OnChildChanged)? Is it when the object is about to be saved (override Save)?
Tuesday, April 06, 2010 7:55:47 AM (Central Standard Time, UTC-06:00)
One thing that comes to mind as I look at this is that there may be some utility in creating something of a rule factory. I would think something like:

var someRule = CslaRuleFactory.CreateRule( customer => customer.Sales < 10, RuleSeverities.Information, "Customer has low sales");

This way, there is an easier glide path from scenarios where rules are expressed as predicates to something that provides the rule itself a context in CSLA that makes it work with the framework.

Just a random thought - there may be other reasons why something like this could not work, but it seems like that would be a nice way to start to separate out some concerns.
Aaron Erickson
Tuesday, April 06, 2010 8:22:49 AM (Central Standard Time, UTC-06:00)
That is an interesting idea Aaron, I hadn't thought of that.

It seems like it should be pretty realistic, though perhaps not exactly as you have it.

public class LambdaRule : BusinessRule
{
public LambdaRule(Csla.Core.IPropertyInfo primaryProperty, Action<RuleContext> rule)
: base(primaryProperty)
{
Rule = rule;
base.RuleUri.AddQueryParameter("r", Rule.GetType().Name);
}

private Action<RuleContext> Rule { get; set; }

protected override void Execute(RuleContext context)
{
Rule(context);
}
}


Then the AddRule() is like this:

BusinessRules.AddRule(new LambdaRule(NameProperty, (context) =>
{
var target = (DataItem)context.Target;
if (target.Name.StartsWith("R"))
context.AddErrorResult("Name can't start with R");
}));

I think this is a good addition – thank you for the idea!!
Tuesday, April 06, 2010 4:37:52 PM (Central Standard Time, UTC-06:00)
Lambda rule looks like a happy medium to me :)

And Rocky sorry if I jumped to conclusions on the authorisation rule side - guess I'm really eager to see what improvements you have in store for that area. CanWrite and CanReadProperty can only take one so far!
In terms of object level authorisation, as long the facility improves to the point where application context/user information can be used in something like a delegate or rule class, I'll be happy.
Was referring to business object context for property level authorisation rules (though CanWrite/CanReadProperty do already provide this in a rudimentary fashion).
Paul
Sunday, April 11, 2010 10:56:04 AM (Central Standard Time, UTC-06:00)
The new rule system looks really great, thanks!

Please ensure that the rules are also working with "PropertyInfo with backing field".

We're using hundreds of classes deriving from BusinessBase which need the backing field, because they are also LinqToSql classes.

The LinqToSQL Column attribute requires the _Storage parameter or performance will suffer considerably.

Unfortunately CheckAllRules is currently always calling ReadProperty, which does not work with "PropertyInfo with backing field".

Do you think this can be resolved?
Horst
Sunday, April 11, 2010 12:06:55 PM (Central Standard Time, UTC-06:00)
My temp hack for fixing the "PropertyInfo with backing field" problem is to modify this function:

protected object ReadPoperty(IPropertyInfo propertyInfo)
{
....
var info = Filedmanager.GetFieldData(propertyInfo)
if (info != null)
return info.Value;
//else
// return null;
//changes:
else
return Utilities.CallByName(this, propertyInfo.Name, CallType.Get);
}
Horst
Monday, April 12, 2010 3:20:11 AM (Central Standard Time, UTC-06:00)
Thanks for making ReadProperty and LoadProperty virtual!
Horst
Monday, April 12, 2010 7:44:10 AM (Central Standard Time, UTC-06:00)
Hello again,
There seems to be a problem with AddRule in the case of your (and also my own) Lambda rule:

Only the last added rule for a property is validated.


//First rule will be ignored:
BusinessRules.AddRule(new LambdaRule(NameProperty, (context) =>
{
var target = (DataItem)context.Target;
if (target.Name.StartsWith("R"))
context.AddErrorResult("Name can't start with R");
}));

BusinessRules.AddRule(new LambdaRule(NameProperty, (context) =>
{
var target = (DataItem)context.Target;
if (target.Name.StartsWith("Y"))
context.AddInfoResult("Name can't start with Y");
}));

If you create another Lambda Rule class (LambdaRule2) and use it for the second rule, both rules will be handled correctly. I didn't debug the problem yet, but just observed it using the WPF property status control.
Horst
Monday, April 12, 2010 9:05:55 AM (Central Standard Time, UTC-06:00)
I hope this is not yet considered spam :).
Here's a reproduction using the current code base:

public class HasTwoLambdaRules : BusinessBase<HasTwoLambdaRules>
{
public static PropertyInfo<string> TestProperty = RegisterProperty<string>(c => c.Test);

public string Test
{
get { return GetProperty(TestProperty); }
set { SetProperty(TestProperty, value); }
}

protected override void AddBusinessRules()
{
BusinessRules.AddRule(new Lambda(TestProperty, context =>
{
var target = (HasTwoLambdaRules) context.Target;
if (target.Test.StartsWith("x"))
context.AddErrorResult("Can't start with x");
}));
BusinessRules.AddRule(new Lambda(TestProperty, context =>
{
var target = (HasTwoLambdaRules) context.Target;
if (target.Test.EndsWith("y"))
context.AddErrorResult("Can't end with y");
}));

var value = (int) ApplicationContext.GlobalContext["Shared"];
ApplicationContext.GlobalContext["Shared"] = ++value;
}

public void Validate()
{
BusinessRules.CheckRules();
}
}
...
[TestMethod]
public void TwoLambdas()
{
UnitTestContext context = GetContext();
ApplicationContext.GlobalContext.Clear();
ApplicationContext.GlobalContext["Shared"] = 0;

var root = new HasTwoLambdaRules();
root.Validate();
context.Assert.AreEqual(string.Empty, root.Test, "Test string should be empty");
context.Assert.AreEqual(0, root.BrokenRulesCollection.Count, "Broken rule count should be 0 first");
root.Test = "x";
context.Assert.AreEqual("x", root.Test, "Test string should be 'x'");
context.Assert.AreEqual(1, root.BrokenRulesCollection.Count, "Broken rule count should be 1");
root.Test = "y";
context.Assert.AreEqual("y", root.Test, "Test string should be 'y'");
context.Assert.AreEqual(1, root.BrokenRulesCollection.Count, "Broken rule count should be 1");
root.Test = "xy";
context.Assert.AreEqual("xy", root.Test, "Test string should be 'xy'");
context.Assert.AreEqual(2, root.BrokenRulesCollection.Count, "Broken rule count should be 2 last");
context.Assert.Success();

context.Complete();
}
Horst
Monday, April 12, 2010 7:49:09 PM (Central Standard Time, UTC-06:00)
I like the idea of the AddOutValue() method.. and you say it eliminates a endless loop. Can you elaborate on technically what you are doing to prevent this?

I'd imagine you either completely disable *any* rules from firing when you apply the OutValues or you just ignore re-firing it's *DependentProperty* rules..

Either way, I'd like to hear more details on the behavior I should expect.
Monday, April 12, 2010 7:56:33 PM (Central Standard Time, UTC-06:00)
AddOutValue() puts the value(s) into a dictionary. When the rule is completed, CSLA loops through the dictionary and calls LoadProperty() on each value. LoadProperty() bypasses all rules processing, so no rules run as part of that process.
Monday, April 12, 2010 7:58:23 PM (Central Standard Time, UTC-06:00)
Thanks Horst (regarding the lambda rule issue). I'll look into that, but I think the problem is with generation of a unique name (rule:// uri) for the lambda. I thought I had a solution to that, but apparently not.
Tuesday, April 13, 2010 1:40:44 PM (Central Standard Time, UTC-06:00)
It seems like bypassing the rules from firing when you mutate state within a rule could put an object into an invalid state.. since now all the rules aren't enforced just because the state change happened from within the object (from a rule) instead of outside the object (from a UI).
Tuesday, April 13, 2010 1:58:42 PM (Central Standard Time, UTC-06:00)
It is true that a rule could put the object into an invalid state.

The thing is, if the rules are re-run (i.e. CSLA uses SetProperty()) then the odds of getting into an infinite loop is near 100% unless the rule author is _extremely_ careful to detect whether the rule should change the state or not.

Consider something trivial like a UCase rule. That rule would need to examine the property value to see if it is already upper case and only alter the property if necessary. And even so, we are guaranteed that this code would run twice any time the rule actually does the change (the first time through when it does the change, and the second time through to find out it shouldn't do anything).

I think I'm choosing between the lesser of two evils here - the author of an async rule needs to know what they are doing to avoid putting the object in an invalid state, vs the author of an async rule nearly always triggering stack overflow exceptions and having their rules run twice for every property change even if they do the right thing.
Tuesday, April 13, 2010 2:30:19 PM (Central Standard Time, UTC-06:00)
Why not just make the engine ignore the rule that made the value change so the endless loop could never happen within the same rule?
Comments are closed.