Extending BusinessRules to Support Client-Side Validation in ASP.NET MVC

Extending BusinessRules to Support Client-Side Validation in ASP.NET MVC

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


NightOwl888 posted on Wednesday, May 01, 2013

I am working on a way to make custom CSLA validation rules work with the extensible validation architecture of ASP.NET MVC. I have worked out how to extend MVC to inject the client-side rules, like this:

1. Create subclass of DataAnnotationsModelValidatorProvider, overriding GetValidators():

        protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context,
                                                                     IEnumerable<Attribute> attributes)
        {
            var items = attributes.ToList();
            if (AddImplicitRequiredAttributeForValueTypes && metadata.IsRequired &&
                !items.Any(a => a is RequiredAttribute))
                items.Add(new RequiredAttribute());

            var validators = new List<ModelValidator>();
            foreach (var attr in items.OfType<ValidationAttribute>())
            {
                // custom message, use the default localization
                if (attr.ErrorMessageResourceName != null && attr.ErrorMessageResourceType != null)
                {
                    validators.Add(new DataAnnotationsModelValidator(metadata, context, attr));
                    continue;
                }

                // specified a message, do nothing
                if (attr.ErrorMessage != null && attr.ErrorMessage != WorkaroundMarker)
                {
                    validators.Add(new DataAnnotationsModelValidator(metadata, context, attr));
                    continue;
                }


                var ctx = new MessageContext(attr, metadata.ContainerType, metadata.PropertyName,
                                                Thread.CurrentThread.CurrentUICulture);
                var errorMessage = validationMessageDataSource.GetMessage(ctx);
                var formattedError = errorMessage == null
                                         ? GetMissingTranslationMessage(metadata, attr)
                                         : FormatErrorMessage(metadata, attr, errorMessage);

                var clientRules = GetClientRules(metadata, context, attr,
                                                 formattedError);
                validators.Add(new LocalizedModelValidator(attr, formattedError, metadata, context, clientRules));
            }


            if (metadata.Model is IValidatableObject)
                validators.Add(new ValidatableObjectAdapter(metadata, context));


            return validators;
        }

2. Create a ValidatableObjectAdapter class that extends ModelValidator. This class uses 2 interfaces IValidatableObject and IClientValidationRule (a custom one) to pull the validation data from CSLA:

    /// <summary>
    /// Adapter which converts the result from <see cref="IValidatableObject"/> to <see cref="ModelValidationResult"/>
    /// </summary>
    /// <remarks>Client side validation will only work if the rules from <see cref="IValidatableObject.Validate"/>
    /// implements <see cref="IClientValidationRule"/></remarks>
    public class ValidatableObjectAdapter
        : ModelValidator
    {
        public ValidatableObjectAdapter(ModelMetadata metadata, ControllerContext controllerContext)
            : base(metadata, controllerContext)
        {
            this.metadata = metadata;
        }

        private readonly ModelMetadata metadata;

        /// <summary>
        /// Gets or sets a value that indicates whether a model property is required.
        /// </summary>
        /// <returns>true if the model property is required; otherwise, false.</returns>
        public override bool IsRequired
        {
            get { return true; }
        }

        /// <summary>
        /// When implemented in a derived class, returns metadata for client validation.
        /// </summary>
        /// <returns>
        /// The metadata for client validation.
        /// </returns>
        /// <remarks>Will only work if the rules from <see cref="IValidatableObject.Validate"/> implements <see cref="IClientValidationRule"/></remarks>
        public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
        {
            var validator = (IValidatableObject)metadata.Model;
            var validationResults = validator.Validate(new ValidationContext(metadata.Model, null, null));
            foreach (var validationResult in validationResults)
            {
                var clientRule = validationResult as IClientValidationRule;
                if (clientRule == null)
                    continue;

                var rule = new ModelClientValidationRule
                {
                    ErrorMessage = clientRule.ErrorMessage,
                    ValidationType = clientRule.ValidationType
                };

                foreach (var kvp in clientRule.ValidationParameters)
                {
                    rule.ValidationParameters.Add(kvp);
                }

                yield return rule;
            }
        }

        /// <summary>
        /// When implemented in a derived class, validates the object.
        /// </summary>
        /// <param name="container">The container.</param>
        /// <returns>
        /// A list of validation results.
        /// </returns>
        public override IEnumerable<ModelValidationResult> Validate(object container)
        {
            var validator = (IValidatableObject)metadata.Model;
            var validationResults = validator.Validate(new ValidationContext(metadata.Model, null, null));
            foreach (var validationResult in validationResults)
            {
                bool hasMemberNames = false;
                foreach (var memberName in validationResult.MemberNames)
                {
                    hasMemberNames = true;
                    var item = new ModelValidationResult
                    {
                        MemberName = memberName,
                        Message = validationResult.ErrorMessage
                    };
                    yield return item;
                }

                if (!hasMemberNames)
                    yield return new ModelValidationResult
                    {
                        MemberName = string.Empty,
                        Message = validationResult.ErrorMessage
                    };
            }
        }
    }

Here is the custom interface:

    public interface IClientValidationRule
    {
        /// <summary>
        /// Gets complete error message (formatted)
        /// </summary>
        string ErrorMessage { get; set; }

        /// <summary>
        /// Gets or sets parameters required for the client validation rule
        /// </summary>
        IDictionary<string, object> ValidationParameters { get; }

        /// <summary>
        /// Gets client validation rule (name of the jQuery rule)
        /// </summary>
        string ValidationType { get; set; }
    }

 

3. Modify CSLA to implement the 2 interfaces, IValidatableObject and IClientValidationRule.

This is where I am running into a wall. Ideally, I would like to make implementing the IClientValidationRule part of the business rule itself so that implementing this optional interface and creating a jQuery library are all that are needed to support client-side validation of CSLA rules.

However, there doesn't seem to be a way to iterate the rules that are registered using BusinessRules.AddRule() to get at those client rules to return in GetClientValidationRules(). Ideally, I would cast the rule to the interface type and return the values that are defined in the rule itself. CSLA seems to only allow access to the broken rules, which don't seem to have any reverse mapping to the rule object that created them. Is there a way to get the original rule type (or better yet, instance) from BrokenRules?

BrokenRules has an internal constructor - I tried subclassing it but its internal constructor sets a private property. Furthermore, the BrokenRules property of BusinessBase is not virtual.

Another question: Why isn't this already part of CSLA? If CSLA had its own ModelValidatorProvider out of the box, people wouldn't need to pull their hair out to do this sort of thing. A mixed bag of client and server-side validation usually isn't acceptable because the server validation rules only fire when the all of the client ones fail. Ideally, we would run them all on the client and with MVC it is possible, provided CSLA had a facility to define the metadata for those rules.

JonnyBee replied on Thursday, May 02, 2013

Hi,

IValidatableObject only exists for .NET 4 and 4.5 - not for Silverlight or WinRT.
IClientValidationRule is from a 3rd party library griffin.mcvccontrib.
IClientValidatable is from System.Web.Mvc namespace and also requires other types from the System.Web.Mvc namespace

CSLA has it's own advanced RuleEngine that can add attribute rules derviced from System.ComponentModel.DataAnnotations.ValidationAttribute into it's rule own engine. And the CSLA RuleEngine runs on all supported platforms - not just a subset where System.Web.Mvc is available.

So from a CSLA perspective - if you want to use javacript validation in your client you should implement your own IClientValidateable rules and javascripts and add these as attributes in your model rather that trying to tweak the CSLA rules into supporting MVC ClientSide validation. 

This may change in the future -  but this is the recommendation for now.

NightOwl888 replied on Thursday, May 02, 2013

Yes, I realize IClientValidatable is the "recommended" way of doing this from Microsoft, but they dropped the ball by including this interface in the System.Web.Mvc DLL. This means there is no way to use it in my business library without setting a hard reference to MVC - something I am trying to avoid. In addition to this snag, once you go down the road of using DataAnnotations attributes exclusively, there is no way to use rule severity, priority, or any of the other enhancements provided by CSLA.

I ended up cannibalizing Griffin.MvcContrib because it was also tying the business layer directly to the presentation layer. However, there is no reason why CSLA couldn't have a similar interface to IClientValidationRule that supports rule severity to enable business rules to optionally support client-side validation. IValidatableObject isn't technically required.

My goal is to extend the CSLA rules subsystem to support client validation, not to change the implementation from the way it is or make it incompatible with other presentation technologies. Unfortunately, with so many private, internal and non-virtual members this task is not easy. I was hoping that you might know a workaround that I couldn't locate. The only thing I really need is a way to iterate the business rules collection so I can cast the rules to IClientValidationRule and pull the metadata from my custom rules - however, it would also be nice if all of the built in CSLA rules also supported client validation metadata by implementing the interface as well.

Short of that, I am left with 5 options. Let me know if you spot any others that I have missed.

1. Add IClientValidationRule and a custom ModelValidatorProvider to CSLA as a contribution so everyone using MVC can benefit from it (without forcing everyone using MVC to tie their business layer directly or indirectly to System.Web.Mvc.dll).

2. Add IClientValidationRule and a custom ModelValidatorProvider to a fork of CSLA and use Git to merge in future enhancements from the CSLA main repository.

3. Make copies of several of the BusinessRules types in my own CSLA extension library and modify them to suit my needs.

4. Make a secondary RulesManager in my own CSLA extension library that can be used in addition to the existing rules system so the rules that are defined can be iterated over. Create overloads of all of the writable base classes that have a custom AddRule() method that wraps BusinessRules.AddRule() and my custom RuleManager.AddRule() into a single method call.

5. Surrender and set a hard reference to System.Web.Mvc.dll from my business layer so I can use IClientValidatable, and pray Microsoft will someday realize the error in their ways and fix it. The damage could be minimized by putting my custom DataAnnotations into a separate assembly so the business layer doesn't reference System.Web.Mvc directly.

 

I am leaning in the direction of going with #4 because it is the least invasive way to do it, has support for all CSLA features, and allows me to upgrade CSLA painlessly, but it has the unfortunate side effect of making all of my CSLA classes conform to a slightly different API call for adding rules.

I am going to rule out #3 because historically the rules system of CSLA has been a moving target and is likely to continue to be. There would be little chance any future enhancements or bug fixes to CSLA business rules would make it into my project.

However, I would also consider making a contribution - either what I describe in #1 or simply by adding a property to BusinessRules so they can be iterated over, provided there is a fair chance the contribution would make it into a future release.

JonnyBee replied on Friday, May 03, 2013

Hi,

There is one more option - which is what were planned to use for this kind of enabling.

A business object may expose the rule descriptions in URI form (and expose through an interface implemented in an intermediate generic base class):

    public IEnumerable<string> GetRuleDescriptions()
    {
      return BusinessRules.GetRuleDescriptions();
    }

And this would return f.ex:

rule://csla.rules.commonrules.dataannotation/Name?a=System.ComponentModel.DataAnnotations.RequiredAttribute
rule://csla.rules.commonrules.dataannotation/Num2?a=System.ComponentModel.DataAnnotations.RangeAttribute
rule://csla.rules.commonrules.maxvalue-system.int32-/Num1?max=5000
rule://businessruledemo.lessthanproperty/Num1?lessthanproperty=Num2
rule://businessruledemo.calcsum/Sum?inputproperties=Num1,Num2
rule://csla.rules.commonrules.minvalue-system.int32-/Sum?min=1
rule://csla.rules.commonrules.maxlength/Name?max=10
rule://csla.rules.commonrules.required/StateName

IE: this is the rule descriptions for the current RuleSet of registered rules for this object.

The first 2 rules are data annotation rules
The next ones is CSLA rules from CommonRules namespace or rules implemented in your app.

Would it be an option to create your own parser of the RuleDescription and create the appropriate mvc classes / javscript mappings in the MVC  Controller? 

The common rules does not include the priority nor is the list sorted in priority order. That could be amended if required.

NightOwl888 replied on Friday, May 03, 2013

Johnny,

Thanks for spotting another option.

However, after looking a little at the particulars, I can see there are a few issues with this approach:

1. This would require a "fix" in the controller for every action that uses it. I am looking for a way to do this as part of the framework so it doesn't have to be solved again later. That said, this approach could still be used with ModelValidatorProvider.

2. Being that the target application will be both multi-tenant and multi-language, and I am already using an approach that uses DataAnnotation or CSLA Business Rule data type combined with Model data type to do the localization lookup, it would be easiest if I had access to the type of business rule rather than creating yet another mapping table that would need to be maintained for every type and potentially every tenant.

3. The ruleType part of the Uri is lowercased and otherwise "fixed", which makes it difficult to construct a reflection call to get an instance of the rule type (required for localization, preferred to get the jQuery function name) based on this URI.

 

I am still inclined to try to take a more direct approach because the primary reason why the current application is being rewritten is because it requires too much maintenance. Ironically, this URI approach solves the most difficult problem - getting the property/value mapping, but makes the easy jQuery function name and error message properties difficult to infer. Creating another collection to track the rule instances or simply adding a property to CSLA make the BusinessRules collection iterable is much simpler from a maintenance standpoint.

BTW - I used the MessageDelegate property of the PropertyRule to solve the localization requirement, but I noticed that this property is mysteriously missing from the ObjectRule class. When localizing, we need to localize all of the validation messages, not just the ones that apply to properties.

 

JonnyBee replied on Friday, May 03, 2013

Hi,

ObjectRules will typically validate the entire object (maybe even call an external rule engine) and probably set different messages on many properties in the BO so that's why there is no MessageDelegate on ObjectRule class.

You are using .NET all the way so you could use reflection. If you import FasterFlect (available on NuGet) and add this extension:

using System;
using System.Collections.Generic;
using System.Linq;
using Csla.Rules;
using Fasterflect;
 
namespace CslaReflect
{
  public static class BusinessRulesExtensions
  {
    public static IEnumerable<IBusinessRule> GetBusinessRules(this Csla.Core.BusinessBase host)
    {
      if (host == nullthrow new ArgumentNullException("host");
 
      var businessRules = (Csla.Rules.BusinessRules) host.GetPropertyValue("BusinessRules");
      var typeRules = (Csla.Rules.BusinessRuleManager) businessRules.GetPropertyValue("TypeRules");
      return typeRules.Rules.AsEnumerable();
    }
  }
}

Then you can use this code to get an enumerable of business rules like this:

      var root = Root.NewEditableRoot();
      var rules = root.GetBusinessRules();
      foreach (var rule in rules)
      {
        Debug.Print(rule.RuleName);
      }

will printout this from my previous example:

rule://csla.rules.commonrules.dataannotation/Name?a=System.ComponentModel.DataAnnotations.RequiredAttribute
rule://csla.rules.commonrules.dataannotation/Num2?a=System.ComponentModel.DataAnnotations.RangeAttribute
rule://csla.rules.commonrules.maxvalue-system.int32-/Num1?max=5000
rule://csla.rules.commonrules.minvalue-system.int32-/Sum?min=1
rule://csla.rules.commonrules.maxlength/Name?max=10
rule://csla.rules.commonrules.required/StateName

NightOwl888 replied on Friday, May 03, 2013

JonnyBee
ObjectRules will typically validate the entire object (maybe even call an external rule engine) and probably set different messages on many properties in the BO so that's why there is no MessageDelegate on ObjectRule class.

That makes sense. I guess I will have to think of another way to do localization if I end up making any object rules. My thought was that object rules would be loosely coupled and therefore only produce one message each. In order to be compatible with MVC they would definitely need 1 rule per message, but for other technologies that might not be the case.

 

Thanks for the tip on FasterFlect. I will give it a try. I am guessing that this inherently means no support for partial trust, but that isn't a strict requirement for my project.

 

BTW - How do you paste formatted code into this forum?

 

JonnyBee replied on Friday, May 03, 2013

Hi,

I use VS 2012 with Blue Color Scheme and Productivity Power Tools extension (one of the features here is HTML Copy). 

Pasting code works fine in Firefox and IE  - for some weird reason not in Chrome

JonnyBee replied on Friday, May 03, 2013

Hi,

I have som concerns as you will only be able to support a subset of the CSLA rule engine.

 

  1. Gateway rules may contain one or more business rules as inner rules and there is no defined way of accessing these inne rules. (ex: CanWrite = nly call inner rule if user has write access). The actual message comes from the inner rules(s). 
  2. A rule may set different messages on a number of fields (this is actually supported).
  3. A rule may update one or more fields with new values.
  4. "Short circuiting" rule may be used as in a "workflow" to stop rule processing (ex: StopIfNotCanWrite = do not validate field if user does not have Write Access)
  5. A rule may read/write properties directly on the object from context.Target
  6. Object rules is never automatically executed - only in CheckRules og explicit call to CheckObjeectRules - so I would not worry too mucth about these in terms on client side validation. 
  7. Transformation rules may change the field value (ex ToUpper, ToLower, RemoveWhitespace)

 

NightOwl888 replied on Friday, May 03, 2013

Thanks for being concerned Smile.

My primary concern is in extending my client side rules beyond what come out of the box with MVC. Actually, what started all of this was a simple requirement to make a property conditionally required based on another property (something Microsoft included in ASP.NET, but for some reason left out of MVC). Actually, there is a way to do it in MVC by removing the error manually from the ModelState, but there is then no way to tell CSLA to ignore the RequiredAttribute on the server side. It didn't make sense to duplicate the entire class just to meet this requirement, which is to support an A/B test scenario. However, I also have other things I want to migrate from the old application. For example, I am doing credit card number validation including number ranges and Luhn algorithm check in JavaScript using an ASP.NET control that I want to port to MVC. Also, there is a serious shortcoming to the way the RegularExpression validators work - namely, that there is no way to exclude meaningless optional values such as spaces or dashes from being evaluated by the regular expression, which either makes the regular expression overly complex or causes usability problems with conforming to a really strict format.

What I would like to achieve is making it possible to implement client side validation from CSLA by simply doing 2 things: 1) implementing IClientValidationRule in the CSLA validation rule and 2) adding a corresponding jQuery library to implement the details on the client.

Some CSLA business rules won't have anything to do with validation, therefore will not implement the IClientValidationRule interface, which means they will be skipped over entirely - that is part of the plan. It wouldn't be too difficult to segregate rules that deal with validation from rules that modify values (loose coupling dictates it should be done this way anyway), and modifying values will either strictly be done on the client or server side - no need to do both. It does sound like I might need to find a way to include more than one error message per rule, though.

I am not sure if I will have any requirements that warrant using Gateway rules or Object rules at this point. I am not as concerned about actually implementing the features of CSLA so much as leaving the door open so it is possible, if needed. The main thing I had my eye on was rule priority, because I can foresee the need to warn the user that something should be filled out that wasn't, but making it required would slow down the data entry process to the point where it is not acceptable. That said, I will probably need to build my own client-side integration point in MVC to take care of that detail, since MVC validation rules are an all or nothing proposition.

JonnyBee replied on Friday, May 03, 2013

Just as a sidenote - you can combine ShortCircuiting rules and others with DataAnnotationRule to f.ex. make a field Required only when some other condition is met as CSLA Rules with a priority less than zero will be executed before the DataAnnotation rules (wich will always have priority = 0).

 

NightOwl888 replied on Friday, May 03, 2013

Thanks for that info. I will try that for this case. But as previously mentioned, it is still worth it to create a way to make client-side validation rules for other cases. I invested 6 weeks in making ASP.NET validation controls before I realized that MVC did things differently. I would like to recoup that investment going forward. I will also review the CSLA e-book section on rules, the MVC rule customization docs, and jQuery validation library docs before actually diving into this so I can map out a definite direction before writing an implementation.

NightOwl888 replied on Sunday, May 05, 2013

JonnyBee
Just as a sidenote - you can combine ShortCircuiting rules and others with DataAnnotationRule to f.ex. make a field Required only when some other condition is met as CSLA Rules with a priority less than zero will be executed before the DataAnnotation rules (wich will always have priority = 0).

 

While this was a good suggestion, in my case it was trading one client validation problem for another because I am also validating the string length of the value when it is not required. So, no dice here either.

 

_pisees_ replied on Thursday, August 14, 2014

JonnyBee

I have some concerns as you will only be able to support a subset of the CSLA rule engine.

 

@JonnyBee continuing on this point. Would your concerns be alleviated if focus just on validation rules, perhaps just what is in csla.rules.commonrules?

Copyright (c) Marimer LLC