Validation Rule Priority

Validation Rule Priority

Old forum URL: forums.lhotka.net/forums/t/8032.aspx


dmbRedGetta posted on Monday, November 23, 2009

I'm using CSLA version 3.7.1 and I'm having trouble with validation rule priority.

I'm trying to use the priority to defer database hits until the other basic rules pass, but it seems that no matter what I do that validation is executed every time.

I read about the new RuleSeverity property, so I added that to the StringRequired rules, but that didn't change anything.

Here's the code:

Protected Overrides Sub AddBusinessRules()

   ValidationRules.AddRule(AddressOf Validation.CommonRules.StringRequired, New Csla.Validation.RuleArgs(LastNameProperty, Validation.RuleSeverity.Error), 0)
   ValidationRules.AddRule(
AddressOf Validation.CommonRules.StringMaxLength, New Validation.CommonRules.MaxLengthRuleArgs(LastNameProperty, 50), 0)

   ValidationRules.AddRule(AddressOf Validation.CommonRules.StringRequired, New Csla.Validation.RuleArgs(FirstNameProperty, Validation.RuleSeverity.Error), 0)
   ValidationRules.AddRule(
AddressOf Validation.CommonRules.StringMaxLength, New Validation.CommonRules.MaxLengthRuleArgs(FirstNameProperty, 50), 0)

   ValidationRules.AddRule(AddressOf Validation.CommonRules.StringRequired, New Csla.Validation.RuleArgs(UserNameProperty, Validation.RuleSeverity.Error), 0)
   ValidationRules.AddRule(
AddressOf Validation.CommonRules.StringMaxLength, New Validation.CommonRules.MaxLengthRuleArgs(UserNameProperty, 50), 0)

   ValidationRules.AddRule(Of User)(AddressOf ValidateUniqueUserName(Of User), UserNameProperty, 1)

   ValidationRules.AddRule(Of User)(AddressOf ValidateGroupID(Of User), GroupIDProperty, 1)

End Sub

Private Shared Function ValidateUniqueUserName(Of T As User)(ByVal target As T, ByVal e As Validation.RuleArgs) As Boolean
   If User.Exists(target.GetProperty(Of String)(UserNameProperty), target.GetProperty(Of Integer)(IDProperty)) Then
      
e.Description = "That UserName has already been used!"
      
e.Severity = Validation.RuleSeverity.Error
      
Return False
   Else
      
Return True
   End If
End Function

Private Shared Function ValidateGroupID(Of T As User)(ByVal target As T, ByVal e As Validation.RuleArgs) As Boolean
   
If target.GetProperty(Of Integer)(GroupIDProperty) = 0 Then
      
e.Description = "Please select a Group for this user."
      
e.Severity = Validation.RuleSeverity.Error
      
Return False
   Else
      
Dim userGroups As ReadOnlyUserGroupList = ReadOnlyUserGroupList.GetUserGroupList()
      
If Not userGroups.Contains(target.GetProperty(Of Integer)(GroupIDProperty)) Then
         
e.Description = "The selected User Group is invalid."
         
e.Severity = Validation.RuleSeverity.Error
         
Return False
      
End If
   
End If
   
Return True
End Function

When I try to save a blank object, I get:

Object is not valid and can not be saved:
-Last Name required
-First Name required
-UserName required
-Please select a Group for this user.

When I try to save an object that only has the UserName field filled out (and populated with a UserName that has already been used), I get:

Object is not valid and can not be saved:
-Last Name required
-First Name required
-Please select a Group for this user.
-That UserName has already been used!

indicating that all of the validation is executing, not just the 0 priority items.

Any ideas?

ajj3085 replied on Monday, November 23, 2009

StringRequired won't cut it, because it won't set e.StopProcessing = true.  You'll need your own rule method, which can delegate to StringRequired, but if e.Result is false will have to set e.StopProcessing to true.  Something like this:

private static bool StringRequiredStop( object target, RuleArgs e ) {

  var result = CommonRules.StringRequired( target, e );

e.StopProcessing = !result;

return result;

}

RockfordLhotka replied on Monday, November 23, 2009

That's not right Andy - it should work.

By default all pri 0 rules run (barring any explicit StopProcessing value).

If any pri 0 rules fail, then NO pri 1+ rules should run.

If all pri 0 rules pass, then ANY failing pri 1+ rule should automatically stop subsequent processing.

ajj3085 replied on Monday, November 23, 2009

Oh really, because I don't think its worked like that for me, that's why I've coded things like that.

RockfordLhotka replied on Monday, November 23, 2009

Here's a simple test app that illustrates how it works. Just change the return value of the Pri0Rule() method to true or false to control whether the Pri1Rule() is invoked.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Csla;

namespace ConsoleApplication1
{
  class Program
  {
    static void Main(string[] args)
    {
      var obj = DataPortal.Create<TestItem>();
      Console.ReadLine();
    }
  }

  [Serializable]
  public class TestItem : BusinessBase<TestItem>
  {
    public static PropertyInfo<string> NameProperty = RegisterProperty<string>(c => c.Name);
    public string Name
    {
      get { return GetProperty(NameProperty); }
      set { SetProperty(NameProperty, value); }
    }

    protected override void AddBusinessRules()
    {
      base.AddBusinessRules();
      ValidationRules.AddRule<TestItem>(Pri0Rule, NameProperty, 0);
      ValidationRules.AddRule<TestItem>(Pri1Rule, NameProperty, 1);
    }

    private static bool Pri0Rule<T>(T target, Csla.Validation.RuleArgs e) where T: TestItem
    {
      Console.WriteLine("Pri0Rule");
      return false;
    }

    private static bool Pri1Rule<T>(T target, Csla.Validation.RuleArgs e) where T : TestItem
    {
      Console.WriteLine("Pri1Rule");
      return true;
    }
  }
}

RockfordLhotka replied on Monday, November 23, 2009

Ahh, I think I see the misunderstanding.

Automatic rule short-circuiting only works within the scope of a single property.

So my earlier post listing how this works applies to rules for a SINGLE property. Each property is evaluated independently from other properties.

If you stop and think about it, this is the only way it can work. A user may set properties in any order, or not at all. When a property is set, that triggers running rules for that property.

When you call ValidationRules.CheckRules() to check all rules, it basically loops through all the properties on the object, calling CheckRules() for each property - still evaluating each property individually (and ignoring dependent properties for efficiency).

dmbRedGetta replied on Monday, November 23, 2009

Yeah, that makes sense.

I still think there is value in avoiding unnecessary database hits whenever possible, so is Andy's solution the best way to accomplish that, or is there a better method?

Thanks for the clarification!

RockfordLhotka replied on Monday, November 23, 2009

I guess the thing to remember is that each property's rules are evaluated independently. So if you want to block a rule for property X, you need a lower priority rule on property X that either returns false or sets StopProcessing=true.

JoeFallon1 replied on Monday, November 23, 2009

dmbRedGetta:

Yeah, that makes sense.

I still think there is value in avoiding unnecessary database hits whenever possible, so is Andy's solution the best way to accomplish that, or is there a better method?

Thanks for the clarification!

Your current code should work as is. No need to change it.

For testing just fill in everything except UserName.

Since StringRequired for UserName is priority 0 it will fail and then the DB hit will not occur since it is priority 1.

(Of course the GroupID database hit will always occur as there are no priority 0 rules for the same property!)

Joe

 

Copyright (c) Marimer LLC