4.1 rules system, room for improvement?

4.1 rules system, room for improvement?

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


JCardina posted on Tuesday, March 08, 2011

I'm migrating a very large commercial app from csla circa 1.x to 4.1.  By far and away the biggest time consumer in porting is the new rules system. 

I expected that but when all is said and done and unit tested the lengths I've had to go to implement what used to be incredibly simple single lines of code is disheartening.  It's now taking many entire classes of custom rules just to replace a line or two of the old rules.

Sadly not just a few lines of simple code but a large swath of completely new code that is a lot of plumbing just to get access to the other properties in the Async rules execute method.

A large commercial, complex app needs a *lot* of rules.  There are the simple standard required and maxlength type rules which of course there are thousands and though we can't use the built in common rules because of the strange decision to include default error messages with no way of specifying our own they are at least simple and straightforward to implement and highly reusable.

But there are also a very large number of highly complex rules that involve multiple properties, possible database calls etc.  

Over the years the CSLA powers that be have quite clearly come to see how crucial these are to a large app but I'm not so sure that those same powers have actually had to write a large complex app with hundreds of custom async rules depending on multiple properties in the business object because they are a big hassle to implement.

It wouldn't be too bad if it were simple to make these custom rules but the tortuous code needed to have access to multiple properties in an async rule is time consuming and error prone.  Surely there is a better way?

I'm sure there's some considerations I'm missing but I hope the rules aren't considered "done" and can be gone over again to see if there are any areas for improvement in coding time with making custom rules. 

Right now I can forsee making a CSLA based app of any complexity is going to be more about writing rules than anything else.

 

 

cds replied on Tuesday, March 08, 2011

I've also experienced some confusion and frustration in writing rules in 4.1 too - and I have the "Using CSLA 4.1 book". There is definitely some clarification needed.

However, I wonder if you could provide some code examples - maybe that would help Rocky and others see what you're talking about and thus find some solution.

Cheers...

Craig

JCardina replied on Tuesday, March 08, 2011

I have the Ebook and all the samples and it's been a real pain to get to the bottom of it but in some ways that is good as you learn more that way, stepping through with a debugger and seeing what's actually happening on the call stack.

I thought about giving samples but it would take up so much space in the post I thought it would be too much but you're right it would help for discussion.

Here's a good example of a low complexity rule from the old version, this rule lives in the setter on the UserType property in my old app:

 BrokenRules.Assert("ClientIDInvalid", "Error.Object.RequiredFieldEmpty,O.Client", "ClientID",
                            (mUserType== UserTypes.Client && mClientID==Guid.Empty));


Note that it must reference another property and does so easily and simply.  

Here is the same rule ported to 4.1:

//User of type client needs a client ID
        public class SettingUserTypeToClientLoginTypeRequiresAClientIdRule : Csla.Rules.BusinessRule
        {
            private Csla.Core.IPropertyInfo mUserTypeProperty { get; set; }
            public SettingUserTypeToClientLoginTypeRequiresAClientIdRule(Csla.Core.IPropertyInfo primaryProperty,
                Csla.Core.IPropertyInfo userTypeProperty)
                : base(primaryProperty)
            {
                mUserTypeProperty = userTypeProperty;               
                InputProperties = new List<Csla.Core.IPropertyInfo>();
                InputProperties.Add(PrimaryProperty);
                InputProperties.Add(userTypeProperty);              
            }

            protected override void Execute(Csla.Rules.RuleContext context)
            {
                var usertype = (UserTypes)context.InputPropertyValues[mUserTypeProperty];
                var clientid = (Guid)context.InputPropertyValues[PrimaryProperty];             

                if (usertype == UserTypes.Client)
                {
                    if (clientid==Guid.Empty)
                        context.AddErrorResult("Error.Object.RequiredFieldEmpty");
                }              
            }
        }//EOC

 

It seems simple enough but it's a custom rule and there are hundreds that depend on certain logic that can't be made generic.  Note the difficulty of accessing the dependent property.  This is a simple rule I've used to save space here, I have many others with 5 or 6 co properties that are required to make rule decisions and many that have to call other business objects and so need to be Async with it's added complexity.  One thing that would really make life easier is some way to async access the data from other properties without all that redundancy.

 

 

 

RockfordLhotka replied on Tuesday, March 08, 2011

I guess I should also point out that synchronous rules aren't subject to nearly the same limitations as async rules. My previous post is all about async, but while I was typing that, you posted a sync rule.

Sync rules can use the same coding structure as async rules. And I usually do use the same structure for consistency. But they surely don't have to.

Most notably, they can use the context.Target property to directly interact with the business object.

I recommend you use the protected ReadProperty and LoadProperty methods from the BusinessRule base class to safely bypass property checks, but you could also use the BypassPropertyChecks object from Target.

Your original rule implementation doesn't appear to do this, and that's a bug in your old code. Your rule could easily run afoul of authorization rules that would cause it to fail...

I guess if you know that you never have authorization rules, you can read property values without fear. But you still can't update properties in a rule without somehow bypassing rules (and PropertyChanged eventing - or you can get infinite looping).

JCardina replied on Tuesday, March 08, 2011

Yeah that rule didn't need the Async style propery accessors but a great many do, and for consistency I use the Async style and it was a nice short one I could post.

The original rule is from Csla 1.x (I think it was 1.4 or 1.5 when I used it) and that's how all the rules are done in that app.  I don't recall any other method of doing business rules from csla 1.x unless I missed something big.  In any case it hasn't been a problem in that app.

I realize there are a lot of considerations, I'm not asking you to justify it, I'm just suggesting that it's very onerous to write those rules by comparison and the actual code to logic being implemented ratio is pretty big which doesn't pass the sniff test of good practices and after all it really is new business rules method 1.0 so surely there is room for improvement somewhere, maybe outside the box?

RockfordLhotka replied on Tuesday, March 08, 2011

I think you can write your example rule like this (assuming reuse isn't possible or desired):

  [Serializable]
  public class Test : BusinessBase<Test>
  {
    public static readonly PropertyInfo<int> ClientIdProperty = RegisterProperty<int>(c => c.ClientId);
    public int ClientId
    {
      get { return GetProperty(ClientIdProperty); }
      set { SetProperty(ClientIdProperty, value); }
    }

    public static readonly PropertyInfo<UserTypes> UserTypeProperty = RegisterProperty<UserTypes>(c => c.UserType);
    public UserTypes UserType
    {
      get { return GetProperty(UserTypeProperty); }
      set { SetProperty(UserTypeProperty, value); }
    }

    protected override void AddBusinessRules()
    {
      base.AddBusinessRules();
      BusinessRules.AddRule(new Csla.Rules.CommonRules.Lambda(ClientIdProperty, (context) =>
        {
          var target = (Test)context.Target;
          using (target.BypassPropertyChecks)
          {
            if (target.UserType == UserTypes.Client && target.ClientId.Equals(Guid.Empty))
              context.AddErrorResult("Error.Object.RequiredFieldEmpty,O.Client");
          }
        }));
    }
  }

  public enum UserTypes
  {
    None,
    Client
  }

Not quite as concise as the Assert approach, but this uses managed backing fields and so the BypassPropertyChecks is required.

JCardina replied on Tuesday, March 08, 2011

RockfordLhotka
I think you can write your example rule like this (assuming reuse isn't possible or desired):

Interesting, thanks for posting that I hadn't thought of doing it that way, perhaps I need to rethink my async style for consistency rule.  Maybe it falls under the category of YAGNI.

ajj3085 replied on Tuesday, March 08, 2011

JCardina
Interesting, thanks for posting that I hadn't thought of doing it that way, perhaps I need to rethink my async style for consistency rule.  Maybe it falls under the category of YAGNI.

This comes up at my employer too.  I think consistency is fine, given all things are equal.  Making some tasks harder just for the sake of consistency though is a bad idea.  To me, doing the absolute simpliest thing that works will always trump consistency.

JCardina replied on Tuesday, March 08, 2011

Consistency in coding has rewarded me so many times over so many years it's an automatic reaction at this point. :)

However every line of code not written is worth a fair bit of real world money for a small shop like ours.  Code not written never breaks and never needs to be maintained, debugged, tested, documented or supported.

RockfordLhotka replied on Tuesday, March 08, 2011

You did actually miss some intervening behaviors, yes :)

From 2005-2010 there was the model introduced in CSLA .NET 2.1 that was delegate based. I suspect most people used that model over the past 5-6 years because it was actually backed by a rule engine.

The 1.x stuff was really nothing - just a way for you to write entries into the broken rules collection - tons of limitations and drawbacks. We were using an MSN group or something back then, and all the discussion history from 2003-2005 is surely lost. But there were (and are) extremely good reasons why I implemented the 2.1 rule engine.

Likewise, over 5-6 years it became quite clear that the 2.1 model had numerous limitations - not least among them that async and sync rules where entirely different, and that writing a reliable async rule took some serious skill. And the use of delegates was limiting because you can't inherit, override, compose, or really test those rules.

The CSLA 4 model may require some more code, but its benefits are pretty dramatic - starting with testability, followed by being able to use OOP principles in their implementation, and ending with the high level of consistency between sync and async rule implementations.

JCardina replied on Tuesday, March 08, 2011

RockfordLhotka
Not quite as concise as the Assert approach, but this uses managed backing fields and so the BypassPropertyChecks is required.

No it's perfect, very elegant solution.  I like it and will rethink how I'm going about this.  Thanks for taking the time to post that.  I knew there were lambda rules but hadn't considered the implications entirely.

Cheers!

RockfordLhotka replied on Tuesday, March 08, 2011

Async is complex. Most developers (including me) are unable to casually write async code and get it right.

(btw, the "powers that be" is me - although some awesome contributions have been made by several people over the past few years, the feature set and design decisions in the framework are ultimately my responsibility)

Microsoft created MTS and later COM+ specifically to enable multi-threaded server code, without making normal developers deal with threading.

ASP.NET does the same thing. As does Windows Workflow (WF).

CSLA 4 follows the same conceptual model as WF, though with a very different implementation. When you spin up a workflow in WF, it runs in isolation. It may run on its own thread, but inside the WF there is exactly one thread, and communication between the workflow and anything outside the workflow is serialized (copied) to avoid two threads referencing the same instance of any object.

CSLA 4 does this too, for async rules. CSLA isn't threadsafe (see this thread: http://forums.lhotka.net/forums/t/10119.aspx) and can't do what it does today with data binding and be threadsafe. Because it isn't threadsafe, you can't interact with a business object graph with more than one thread at a time. And if the object graph (any part of it) is bound to a UI, you can only interact with it on the UI thread.

At the same time, sometimes rules need to be async. Mostly because they interact with the server asynchronously - at least that is the design goal of the current implementation. Secondarily, they might be async because they implement a long-running algorithm ("long-running" being seconds, not hours or days - that would require a workflow with persistence).

If a rule is running (in whole or part) on a background thread, then it can't interact with the business object. Or at least I must assume it can't. The reason I must make this assumption, is that in most cases the object is bound to a UI, so it is absolutely illegal for a background thread to interact with the object. If you try (and you can bypass my protections), you'll cause cross-thread exceptions from .NET itself.

On the other hand, if you absolutely know for a fact that no other thread will use the object graph while the rule is running, and that the object graph is not bound to a UI, then you can bypass the protections. You can tell the rule to turn on the Target property, and your async rule can interact with the object all it wants - directly reading and setting the object's properties.

Also, if you absolutely know for a fact that your rule code will only be running in Silverlight, you can bypass the protections. This is because WCF marshals callbacks onto the UI thread for you (in Silverlight). So in reality, even the async callback code is on the UI thread, so it is safe to interact directly with the object's properties. Clearly, if you go down this road and later use the business classes in WPF or ASP.NET you'd encounter all sorts of nasty cross-threading issues.

The limitations I'm describing here aren't things I made up for fun. In fact, I wish it could be simpler. But threading and async code is not simple. The best solutions thus far have centered around frameworks or runtimes providing isolation between threads so a developer has a hard time doing the wrong thing. CSLA 4 follows that conceptual model.

We know (based on last year's Microsoft PDC) that Microsoft intends to provide some nice new threading/async features in the next version of .NET (and the next versions of C# and VB). It is possible (likely) that these new features will simplify the code used to invoke and implement async algorithms.

Even those features don't eliminate the realities of threading - race conditions, deadlocks, etc. They simplify async quite a lot, by making async code appear to execute in a linear and synchronous manner. So (if the features end up in Silverlight in a timely manner) they'll be extremely useful for async work. In fact, they'll fundamentally alter the way things like the async data portal (and therefore async rules) operate.

But they won't really simplify true threading code. The basic rules of isolation remain intact - multiple threads interacting with any shared object or value must either be avoided, or the object/value must be carefully wrapped with locking structures.

What does all this mean?

It means that somewhere in the next few years, as these new async runtime/language features roll out in .NET, Silverlight, and WP7 (and presumably mono, etc), CSLA version X will be able to take advantage of them. This almost certainly means two things:

  1. async code (like the async data portal and rules) will get simpler
  2. async code won't look or work the way it does today, so you'll need to update your rules to take advantage of the new capabilties and language keywords

I suspect it will have some impact on actual background threading code too - in that it may simplify the invocation and callback handling code. But it is unlikely that it will change the fact that a background thread can't directly interact with properties on an object bound to a UI.

tiago replied on Tuesday, March 08, 2011

I saw your post today, as I saw the post in CslaGenFork forum http://cslagenfork.codeplex.com/discussions/240643

I started working on this subject on the spot (it's Mardi Gras holiday after all). I got back here and found the discussion progressed a lot. Nevertheless I want to have my saying.

JCardina

A large commercial, complex app needs a *lot* of rules.  There are the simple standard required and maxlength type rules which of course there are thousands and though we can't use the built in common rules because of the strange decision to include default error messages with no way of specifying our own they are at least simple and straightforward to implement and highly reusable.

 

Add your own base CommonBusinessRule abstract class:

    /// <summary>
    /// Base class used to create common rules.
    /// </summary>
    public abstract class CommonBusinessRule : BusinessRule
    {
        /// <summary>
        /// Gets or sets the severity for this rule.
        /// </summary>
        public RuleSeverity Severity { getset; }
 
        /// <summary>
        /// Gets or sets the error message for this rule.
        /// </summary>
        public string ErrorMessage { getset; }
 
        /// <summary>
        /// Creates an instance of the rule.
        /// </summary>
        /// <param name="primaryProperty">Primary property.</param>
        public CommonBusinessRule(IPropertyInfo primaryProperty)
            : base(primaryProperty)
        {
            Severity = RuleSeverity.Error;
        }
 
        /// <summary>
        /// Creates an instance of the rule.
        /// </summary>
        /// <param name="primaryProperty">Primary property.</param>
        /// <param name="errorMessage">Error message.</param>
        public CommonBusinessRule(IPropertyInfo primaryProperty, string errorMessage)
            : base(primaryProperty)
        {
            ErrorMessage = errorMessage;
        }
 
        /// <summary>
        /// Creates an instance of the rule.
        /// </summary>
        public CommonBusinessRule()
        {
            Severity = RuleSeverity.Error;
        }
    }

Now rewrite some common business rules in order make use the resource string only when you don't supply an error message.

Example for Required rule

    /// <summary>
    /// Business rule for a required string.
    /// </summary>
    public class Required : CommonBusinessRule
    {
        /// <summary>
        /// Creates an instance of the rule.
        /// </summary>
        /// <param name="primaryProperty">Property to which the rule applies.</param>
        public Required(IPropertyInfo primaryProperty)
            : base(primaryProperty)
        {
            ErrorMessage = Resources.StringRequiredRule;
            InputProperties = new List<IPropertyInfo> { primaryProperty };
        }
 
        /// <summary>
        /// Creates an instance of the rule.
        /// </summary>
        /// <param name="primaryProperty">Property to which the rule applies.</param>
        /// <param name="errorMessage">The error message.</param>
        public Required(IPropertyInfo primaryProperty, string errorMessage)
            : base(primaryProperty, errorMessage)
        {
            InputProperties = new List<IPropertyInfo> { primaryProperty };
        }
 
        /// <summary>
        /// Rule implementation.
        /// </summary>
        /// <param name="context">Rule context.</param>
        protected override void Execute(RuleContext context)
        {
            var value = context.InputPropertyValues[PrimaryProperty];
#if WINDOWS_PHONE
            if (value == null || string.IsNullOrEmpty(value.ToString()))
#else
            if (value == null || string.IsNullOrWhiteSpace(value.ToString()))
#endif
            {
                var message = string.Format(ErrorMessagePrimaryProperty.FriendlyName);
                context.Results.Add(new RuleResult(RuleNamePrimaryProperty, message) { Severity = Severity });
            }
        }
    }

Example of usage of the new required rule

    protected override void AddBusinessRules()
    {
        base.AddBusinessRules();
        BusinessRules.AddRule(new Required(DocRefProperty"I need {0}!") {Severity = RuleSeverity.Warning});
        BusinessRules.AddRule(new Required(DocDateProperty));
        BusinessRules.AddRule(new Required(SubjectProperty));
    }

Example for an attribute rule

    /// <summary>
    /// Business rule for checking a date property is valid and not in the future.
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public class DateNotInFutureAttr : ValidationAttribute
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="DateNotInFutureAttr"/> class.
        /// </summary>
        public DateNotInFutureAttr()
            : base()
        {
            ErrorMessage = "{0} can't be in the future.";
        }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="DateNotInFutureAttr"/> class.
        /// </summary>
        /// <param name="errorMessage">The error message.</param>
        public DateNotInFutureAttr(string errorMessage)
            : base(errorMessage)
        {
            ErrorMessage = errorMessage;
        }
 
        /// <summary>
        /// Validation rule implementation.
        /// </summary>
        /// <param name="value">The date to validate.</param>
        /// <param name="validationContext">The context information about the validation operation.</param>
        /// <returns>
        /// An instance of the <see cref="T:System.ComponentModel.DataAnnotations.ValidationResult"/> class.
        /// </returns>
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            if ((SmartDate)value > DateTime.Now)
            {
                string message = (string.Format(ErrorMessage, validationContext.DisplayName));
                return new ValidationResult(message);
            }
 
            try
            {
                if ((SmartDate)value >= DateTime.MinValue)
                {
                    return null;
                }
            }
            catch (Exception ex)
            {
                return new ValidationResult(
                    string.Format(string.Format("{0}{1} isn't valid.",
                                                ex.Message + Environment.NewLine,
                                                validationContext.DisplayName)));
            }
            return null;
        }
    }

example usage of attribute rule

    /// <summary>
    /// Maintains metadata about <see cref="DocDate"/> property.
    /// </summary>
    private static readonly PropertyInfo<SmartDateDocDateProperty = RegisterProperty<SmartDate>(p => p.DocDate"Date Doc");
    /// <summary>
    /// Gets or sets the Date Doc.
    /// </summary>
    /// <value>The Date Doc.</value>
    [DateNotInFutureAttr("Please pay attention: {0} can't be in the future.")]
    public string DocDate
    {
        get { return GetPropertyConvert<SmartDate,String>(DocDateProperty); }
        set { SetPropertyConvert<SmartDate,String>(DocDatePropertyvalue); }
    }

 

I believe you have a point in that the Csla Rule System 4.1 should include an error message property. As it doesn't I'm planning to re-write all common rules as for the Required sample above and propose Jonny to include them in CslaContrib

JonnyBee replied on Wednesday, March 09, 2011

Hi,

You should be aware that your rules as described here will only support one CultureInfo. They will not work properly in a web application that supports several cultures or a Silverlight application that supports several Cultures with at common CSLA serverside DataAccess.

This is because the ErrorString is set in the constructor and that very instance of the rule is common for ALL instances of the class no matter which culture your thread is running. So the current culture when the object is first accessed is the winner.

Consider adding a ErrorMessageDelegate ( a Func<string>) that will be called in ErrorMessage property when the rule executes:

 
  /// <summary>
  /// Base class used to create common rules.
  /// </summary>
  public abstract class CommonBusinessRule : BusinessRule
  {
    /// <summary>
    /// Gets or sets the severity for this rule.
    /// </summary>
    public RuleSeverity Severity { getset; }

    /// <summary>
    /// Gets the error message.
    /// </summary>
    public string ErrorMessage { 
      get
      {
        return ErrorMessageDelegate.Invoke();
      }
    }

    /// <summary>
    /// Gets or sets the error message function this rule.
    /// </summary>
    protected Func<string> ErrorMessageDelegate { getset; }

    /// <summary>
    /// Creates an instance of the rule.
    /// </summary>
    public CommonBusinessRule()
    {
      Severity = RuleSeverity.Error;
    }

    /// <summary>
    /// Creates an instance of the rule.
    /// </summary>
    /// <param name="primaryProperty">Primary property.</param>
    public CommonBusinessRule(IPropertyInfo primaryProperty): base(primaryProperty)
    {
      Severity = RuleSeverity.Error;
    }

    /// <summary>
    /// Creates an instance of the rule.
    /// </summary>
    /// <param name="primaryProperty">Primary property.</param>
    /// <param name="errorMessageDelegate">Error message.</param>
    public CommonBusinessRule(IPropertyInfo primaryProperty, Func<string> errorMessageDelegate)
      : base(primaryProperty)
    {
      ErrorMessageDelegate = errorMessageDelegate;
    }
  }

And Required rule like this:
  /// <summary>
  /// Business rule for a required string.
  /// </summary>
  public class RequiredRule : CommonBusinessRule
  {
    /// <summary>
    /// Creates an instance of the rule.
    /// </summary>
    /// <param name="primaryProperty">Property to which the rule applies.</param>
    public RequiredRule(IPropertyInfo primaryProperty)
      : base(primaryProperty)
    {
      // set default delegate
      ErrorMessageDelegate = (() => Resources.StringRequiredRule);
      InputProperties = new List<IPropertyInfo> { primaryProperty };
    }

    /// <summary>
    /// Creates an instance of the rule.
    /// </summary>
    /// <param name="primaryProperty">Property to which the rule applies.</param>
    /// <param name="errorMessage">The error message.</param>
    public RequiredRule(IPropertyInfo primaryProperty, Func<string> errorMessage)
      : base(primaryProperty, errorMessage)
    {
      InputProperties = new List<IPropertyInfo> { primaryProperty };
    }

    /// <summary>
    /// Rule implementation.
    /// </summary>
    /// <param name="context">Rule context.</param>
    protected override void Execute(RuleContext context)
    {
      var value = context.InputPropertyValues[PrimaryProperty];
#if WINDOWS_PHONE
            if (value == null || string.IsNullOrEmpty(value.ToString()))
#else
      if (value == null || string.IsNullOrWhiteSpace(value.ToString()))
#endif
      {
        var message = string.Format(ErrorMessage, PrimaryProperty.FriendlyName);
        context.Results.Add(new RuleResult(RuleName, PrimaryProperty, message) { Severity = Severity });
      }
    }
  }

Usage of rule:
      BusinessRules.AddRule(new RequiredRule(NameProperty, () => Resources.MyRequiredMessage));

This will ensure that the proper localized string will always be returned from a resource file.

 

 

 

tiago replied on Wednesday, March 09, 2011

Good point Jonny. Going back to my above example, if I don't want to use resources and use just a plain string I can use it like this

BusinessRules.AddRule(new Required(DocRefProperty, () => "I need {0}!") {Severity = RuleSeverity.Warning});

 

 

tiago replied on Wednesday, March 09, 2011

Jonny,

Proposed changes to common rules are on the post.

tiago replied on Saturday, April 02, 2011

Hi all,

Just dropping a word to let you know that the changes discussed in this thread were published in CslaContrib changeset #86959 with the comment "Added reimplemented Csla CommonRules that accepts a Func<string> to set a custom ErrorMessage"

Thanks Jonny.

xal replied on Sunday, June 12, 2011

Hey all!

It's been a while since I posted anything. Anyway, I'm in the process of migrating some old apps, working on some newer code generation, etc, and I found this post while looking for migration topics.

I wanted to share, just for reference, something that's been really helpful for migrating rules to the new model. I used a lot of lambdas before and I think the ruledelegate approach was pretty good for most cases, and I don't see why it couldn't be maintained in a "masked" fashion, such the one shown below, using extension methods (one localizable and one for hard coded strings).

This way, I've been migrating rules into a friendlier:

BusinessRules.AddRule(Of Invoice)(InvoiceAmountProperty, Function(t) t.InvoiceAmount >= 0, "Invoice amount can't be negative.")

BusinessRules.AddRule(Of Invoice)(InvoiceAmountProperty, Function(t) t.InvoiceAmount >= 0, Function() My.Resources.ruleErrorInvoiceAmount)

 

Code:
 =============================================================
    <System.Runtime.CompilerServices.Extension()> _
    Public Sub AddRule(Of T As BusinessBase(Of T))(ByVal br As BusinessRules,
                                                   ByVal prop As Csla.Core.IPropertyInfo,
                                                   ByVal ruleHandler As Func(Of T, Boolean),
                                                   ByVal description As String)
        Dim rule As New CommonRules.Lambda(prop,
                Sub(o)
                    Dim target = DirectCast(o.Target, T)
                    Using GetBypassPropertyChecks(target)
                        If Not ruleHandler(target) Then
                            o.AddErrorResult(description)
                        End If
                    End Using
                End Sub)
        br.AddRule(rule)
    End Sub

    <System.Runtime.CompilerServices.Extension()> _
    Public Sub AddRule(Of T As BusinessBase(Of T))(ByVal br As BusinessRules,
                                                   ByVal prop As Csla.Core.IPropertyInfo,
                                                   ByVal ruleHandler As Func(Of T, Boolean),
                                                   ByVal localizableDescription As Func(Of String))
        Dim rule As New CommonRules.Lambda(prop,
                Sub(o)
                    Dim target = DirectCast(o.Target, T)
                    Using GetBypassPropertyChecks(target)
                        If Not ruleHandler(target) Then
                            o.AddErrorResult(localizableDescription())
                        End If
                    End Using
                End Sub)
        br.AddRule(rule)
    End Sub

 

   Private ReadOnly m_BypassPropertyInfo As System.Reflection.PropertyInfo = GetType(Csla.Core.BusinessBase).GetProperty("BypassPropertyChecks", BindingFlags.Public Or BindingFlags.NonPublic Or BindingFlags.Instance)
    Function GetBypassPropertyChecks(target As Csla.Core.BusinessBase) As IDisposable
        Return DirectCast(m_BypassPropertyInfo.GetValue(target, Nothing), IDisposable)
    End Function

=============================================================

 

Comments appreciated!

 

Cheers!

Andrés

xal replied on Monday, June 13, 2011

Even simpler migration.

Below, there's an example of how you could write a couple of extension methods that would magically make most of your existing rules work. Of course, this is the most basic example and it could be extended to have severity and the rest of the features, but I think this example will cover the basics and you could extend as needed. All you'd have to do then is run a Find/Replace for Csla.Validation to Csla.Rules. This way you could take your time to port your rules to the new method properly.

I think this should work for most people, but again, comments appreciated.

 

First create a Class such as this:

<Serializable()>
Public Class RuleArgs
    Public Property Description() As String
End Class

 

<System.Runtime.CompilerServices.Extension()> _
    Public Sub AddRule(Of T As BusinessBase(Of T))(ByVal br As BusinessRules,
                                                   ByVal ruleHandler As Func(Of T, RuleArgs, Boolean),
                                                   ByVal args As RuleArgs,
                                                   ByVal prop As Csla.Core.IPropertyInfo)
        Dim rule As New CommonRules.Lambda(prop,
                Sub(o)
                    Dim target = DirectCast(o.Target, T)
                    Using GetBypassPropertyChecks(target)
                        If Not ruleHandler(target, args) Then
                            o.AddErrorResult(args.Description) ' Select Case for severity here
                        End If
                    End Using
                End Sub)
        br.AddRule(rule)
    End Sub


    <System.Runtime.CompilerServices.Extension()> _
    Public Sub AddRule(Of T As BusinessBase(Of T))(ByVal br As BusinessRules,
                                                   ByVal ruleHandler As Func(Of T, RuleArgs, Boolean),
                                                   ByVal prop As Csla.Core.IPropertyInfo)
        AddRule(br, ruleHandler, New RuleArgs, prop)
    End Sub

 

'''' Overloads for object rules, not that this could take an extra string, for what previously was the "property name".

''' This isn't used anymore, but would make your existing AddRule calls work.

    <System.Runtime.CompilerServices.Extension()> _
    Public Sub AddRule(Of T As BusinessBase(Of T))(ByVal br As BusinessRules,
                                                   ByVal ruleHandler As Func(Of T, RuleArgs, Boolean),
                                                   ByVal args As RuleArgs)
        Dim rule As New CommonRules.Lambda(
                Sub(o)
                    Dim target = DirectCast(o.Target, T)
                    Using GetBypassPropertyChecks(target)
                        If Not ruleHandler(target, args) Then
                            o.AddErrorResult(args.Description)
                        End If
                    End Using
                End Sub)
        br.AddRule(rule)
    End Sub

    <System.Runtime.CompilerServices.Extension()> _
    Public Sub AddRule(Of T As BusinessBase(Of T))(ByVal br As BusinessRules,
                                                   ByVal ruleHandler As Func(Of T, RuleArgs, Boolean))
        AddRule(br, ruleHandler, New RuleArgs)
    End Sub

Private ReadOnly m_BypassPropertyInfo As System.Reflection.PropertyInfo = GetType(Csla.Core.BusinessBase).GetProperty("BypassPropertyChecks", BindingFlags.Public Or BindingFlags.NonPublic Or BindingFlags.Instance)
    Function GetBypassPropertyChecks(target As Csla.Core.BusinessBase) As IDisposable
        Return DirectCast(m_BypassPropertyInfo.GetValue(target, Nothing), IDisposable)
    End Function

 

 

Andrés

RockfordLhotka replied on Monday, June 13, 2011

I added this idea to the wish list.

http://www.lhotka.net/cslabugs/edit_bug.aspx?id=936

xal replied on Monday, June 13, 2011

Thank you Rocky. But just to make it clear to others, this could be added into your own project today as it is, without altering Csla.

And even if it is finally added to Csla, it's unlikely that RuleArgs will get reinstated, so this workaround could still be used to ease the pain of migration (at least partially).

 

Andrés

Copyright (c) Marimer LLC