MarimerLLC / cslaforum

Discussion forum for CSLA .NET
https://cslanet.com
Other
31 stars 6 forks source link

Business Rule + Validation Rule #15

Closed halloween8 closed 9 years ago

halloween8 commented 9 years ago

I have an async business rule that gets data from a table and sets other properties of the object, after these are set I want to validate that the required properties are set.

Here is what I did something like this

public class AsyncBusinessRule : BusinessRule { private Csla.Rules.IBusinessRule InnerRule1 { get; set; }

        public AsyncBusinessRule(IPropertyInfo primaryProperty, IPropertyInfo secondProperty, IPropertyInfo thirdProperty, IPropertyInfo forthProperty, IPropertyInfo fifthProperty, IPropertyInfo sixthProperty, IBusinessRule innerRule1)
            : base(primaryProperty)
        {
            this.InputProperties = new List<IPropertyInfo> { primaryProperty, secondProperty, thirdProperty, forthProperty, fifthProperty, sixthProperty };

            this.AffectedProperties.Add(primaryProperty);
            this.AffectedProperties.Add(secondProperty);
            this.AffectedProperties.Add(thirdProperty);
            this.AffectedProperties.Add(forthProperty);
            this.AffectedProperties.Add(fifthProperty);

            InnerRule1 = innerRule1;

            IsAsync = true;

            this.ProvideTargetWhenAsync = true;
        }

        protected async override void Execute(RuleContext context)
        {
            var purchaseInvoiceIDProperty = this.AffectedProperties[1];
            var purchaseInvoiceID = (string)context.InputPropertyValues[purchaseInvoiceIDProperty];

            var invoiceIDProperty = this.AffectedProperties[2];
            var invoiceID = (string)context.InputPropertyValues[invoiceIDProperty];

            var supplierUIDProperty = this.AffectedProperties[3];
            var supplierUID = (Guid?)context.InputPropertyValues[supplierUIDProperty];

            var invoiceDateProperty = this.AffectedProperties[4];
            var invoiceDate = (DateTime)context.InputPropertyValues[invoiceDateProperty];

            var purchaseInvoiceUIDProperty = this.AffectedProperties[5];

            var isAInOutProperty = this.InputProperties[5];
            var isAInOut = (byte)context.InputPropertyValues[isAInOutProperty];

            IPurchaseInvoiceInfoList purchaseInvoiceInfoList = null;

            try
            {
                if (!string.IsNullOrWhiteSpace(purchaseInvoiceID) || !string.IsNullOrWhiteSpace(invoiceID) || (supplierUID != null && supplierUID != Guid.Empty))
                {
                    purchaseInvoiceInfoList = await purchaseInvoiceInfoListFactory.FetchAsync(new PurchaseInvoiceCriteria() { PurchaseInvoiceID = purchaseInvoiceID, InvoiceID = invoiceID, SupplierUID = (Guid)supplierUID });

                    if (purchaseInvoiceInfoList != null && purchaseInvoiceInfoList.Any())
                    {
                        if (purchaseInvoiceInfoList.First().StatusFlag <= Constants.StatusFlagClosed)
                        {
                            context.AddOutValue(purchaseInvoiceUIDProperty, purchaseInvoiceInfoList.First().PurchaseInvoiceUID);
                            context.AddOutValue(purchaseInvoiceIDProperty, purchaseInvoiceInfoList.First().PurchaseInvoiceID);
                            context.AddOutValue(invoiceIDProperty, purchaseInvoiceInfoList.First().InvoiceID);
                            context.AddOutValue(supplierUIDProperty, purchaseInvoiceInfoList.First().SupplierUID);
                            context.AddOutValue(invoiceDateProperty, purchaseInvoiceInfoList.First().InvoiceDate);

                            var chainedContext = context.GetChainedContext(this.InnerRule1);
                            this.InnerRule1.Execute(chainedContext);
                         }
                        else
                        {
                            context.AddErrorResult(ErrorConstants.ERROR_THIS_PURCHASE_INVOICE_IS_CLOSED);
                        }
                    }
                }

            }
            catch (Exception e)
            {
                Debug.WriteLine("Business rule AsyncBusinessRule Error {0} ", e);
            }
            finally
            {
                context.Complete();
            }
        }
    }

The problem seems to be that this code var chainedContext = context.GetChainedContext(this.InnerRule1); this.InnerRule1.Execute(chainedContext);

The inner rule contains the original value and not the affected one, if I put context.Complete(); before I call the GetChaingedContext.

What Am I doing wrong?

jonnybee commented 9 years ago

My recommendation is to separate business and validation (ex: async lookup and validation).

The async rule should only be responsible for doing the async lookup and update field values and should only be allowed to run when the actual property has been edited by a user. Use PropertyRule base class and :

  CanRunAsAffectedProperty = false;
  CanRunOnServer = false;
  CanRunInCheckRules = false; 

Each field that is updated should have sync rules for validation that should be rerun automatically by the rule engine after the async rule has completed.

See my blog post for more detailed info on the rule engine: https://jonnybekkum.wordpress.com/2011/08/29/csla-4-2-rules-update/ https://jonnybekkum.wordpress.com/2012/11/07/csla-4-5-ruleengine-update/

Now the behavior you see is intended and as such: a) InputProperties is read from the object before the async rule starts execution b) When rule has completed you must call context.Complete and only then will properties be updated and eventual broken rules added to the objects collection of BrokenRules. c) So given this behavior you should prefer to have sync rules to do the basic input format/range/required style validation.

halloween8 commented 9 years ago

Hoooo That worked, but I'm guessing that could be dangerous about infinite loop of business rule checking.

jonnybee commented 9 years ago

Nope. Read my blog posts. By default the rule engine will only rerun but not cascade down when running rules for AffectedPropertied (the second level).

You may activate the advanced mode (CascadeOnDirtyProperties) at your own risk and that may result in an infinite loop.

halloween8 commented 9 years ago

I see, I've removed from the Async rule the InnerRule and added the CanRunAsAffectedProperty = True in the validation rules.

Does not seem to be executed when the context.Complete() is execute.

any thoughts?

jonnybee commented 9 years ago

Well, the question is - when should the async rule run?

My recommendation is that async rules should only run when the user edits fields and so the rule should be added to each input field the user is allowed to edit that may cause the rule to execute.

IE: Async rules should only run on the primary property (1st level) and then the rule engine will rerun sync rules for the next level in context.Complete().

I also prefer to have Async rules registered with a Priority = 10 (or larger). This way the async rule will only run if there are no sync validation errors on the actual field.

When on the logical server side - in data access - you should load fields as appropriate directly from database and NOT rely on async rules.

So if the sync rules does not run the most likely cause is that the async rule is called from the "second" level as AffectedProperty.

halloween8 commented 9 years ago

I understand, but here is what I'm trying to do, I have a screen, where any of the 4 first fields entered can load the rest of the object. The desired behavior is if the user enters the first, second, third of 4 field and that the row is already in the db I load the rest of the object and the user can continue with the rest of the fields. So I what I did is a business rule that load the data and the object based on those fields.

Then I need the object to validate itself with simple sync rule (Required and the like).

So If I understand you correctly, what you are saying is

  1. Change that for a dataportal fetch for all properties and handle the entry (UI) differently if the object is loaded or not? This seems to be a lot more work. OR
  2. For each property that can load the object (could be a combination of the 4 I mention above, call the rule that loads the data so that it can run the affected property For example Property1 Property2 Property3 Property4

AsyncRule(property1, property2,property3,property4) AsyncRule(property2, property1,property3,property4) AsyncRule(property3, property1,property2,property4) AsyncRule(property4, property1,property2,property3)

so these will trigger the validation rule for Property1 when the first rule is trigger?

jonnybee commented 9 years ago

For this case it is #2 and you should also set Priority to be > 0 (I prefer to use 10). This is to make sure async rule is not even called if there are broken rules with lower priority. By default - ALL rules with Priority =0 will always execute. It doesn't matter i previous rules were broken!

so these will trigger the validation rule for Property1 when the first rule is trigger?

No. The AsyncRule for that property will be called when that property is set to a new value. And when you make sure that the async rule is only called when a user edits the field (as primaryProperty) the rule engine will rerun rules for the affectedProperties of this rule. And the async rule should NOT be triggered again to o an async lookup when it is the second run (affectedProperty).

I would declare the rule like this:

public class AsyncRule : PropertyRule
{
    private readonly PropertyInfo<string> _purchaseInvoiceIdProperty;
    private readonly PropertyInfo<string> _invoiceIdProperty;
    private readonly PropertyInfo<Guid?> _supplierIdProperty;
    private readonly PropertyInfo<DateTime> _invoiceDateProperty;
    private readonly PropertyInfo<byte> _isAInOutProperty;

    public AsyncRule(IPropertyInfo primaryProperty, 
                     PropertyInfo<string> purchaseInvoiceIdProperty,
                     PropertyInfo<string> invoiceIdProperty,
                     PropertyInfo<Guid?> supplierIdProperty,
                     PropertyInfo<DateTime> invoiceDateProperty,
                     PropertyInfo<byte> isAInOutProperty) 
        : base(primaryProperty)
    {
        //keep parameters in private variables
        _purchaseInvoiceIdProperty = purchaseInvoiceIdProperty;
        _invoiceIdProperty = invoiceIdProperty;
        _supplierIdProperty = supplierIdProperty;
        _invoiceDateProperty = invoiceDateProperty;
        _isAInOutProperty = isAInOutProperty;

        InputProperties.AddRange(new IPropertyInfo[] { purchaseInvoiceIdProperty, 
            invoiceIdProperty, supplierIdProperty, invoiceDateProperty, isAInOutProperty });
        AffectedProperties.AddRange(new IPropertyInfo[] { purchaseInvoiceIdProperty, 
            invoiceIdProperty, supplierIdProperty, invoiceDateProperty, isAInOutProperty });
        IsAsync = true;

        CanRunAsAffectedProperty = false; // do not run as affected property
        CanRunInCheckRules = false;  // do not run in CheckRules 
        CanRunOnServer = false;      // do not run on serverside
    }

    protected override void Execute(RuleContext context)
    {
        // get input property values using generic helper methods in context
        var purchaseInvoiceID = context.GetInputValue(_purchaseInvoiceIdProperty);
        var invoiceId = context.GetInputValue(_invoiceIdProperty);
        var supplierId = context.GetInputValue(_supplierIdProperty);
        var invoiceDateProperty = context.GetInputValue(_invoiceDateProperty);
        var isAInOutProperty = context.GetInputValue(_isAInOutProperty);

        // do async lookup and set properties 

    }
}

And also make sure that primaryProperty is not used within the Execute method. This will allow you to attach the rule to "multiple" properties as the primaryProperty is only used to control when the rule is executed.

Also note how I added strong typed property info's to the constructor. This allows me to keep the generic property info in member variables and in turn simplifies reading values from InputProperties. It also makes it easier to maintain as you will get a compilation error if you refactor a datatype or tries to use a wrong property type.

halloween8 commented 9 years ago

I see, now what if I have a property 5 which is not needed to load the object but it is set via the AsyncRule and I need to validate that it is valid. The validation rule will never be triggered as it is not a primary property of the Async Rule.

How can I trigger that validation rule on a property that does NOT trigger the async rule but it loaded by it.

jonnybee commented 9 years ago

Again - read my blog posts!

Remember - the async rules does not do validation - only lookup and (in my opinion) should only be called after a user has changed the value of PrimaryProperty.

  1. Make sure the AsyncRule runs for the input properties user can edit and only when PrimaryProperty is changed (and not as AffectedProperties).
  2. The rule engine will rerun (and should be sync validation rules) rules for AffectedProperties from AsyncRule but will not cascade further levels down.
  3. Activate the advanced mode (CascadeOnDirtyProperties) if - and only if - you want the rule engine to run recuresively.
halloween8 commented 9 years ago

Very helpful thanks