Sep 27th 2009 09:44 am

UI and Business Validation with Linq to SQL and T4

Recently I’ve been working on adding a validation framework to our Linq to Sql based business layer and Asp.Net based UI. While L2Sql provides an “OnValidate” partial method to validate changes before they’re persisted, I needed to catch validation errors much earlier in the stack. My goals were to:

  1. Implement the “OnValidate” method and throw a custom exception that contains structured information about the invalid properties
  2. Use the information stored in the dbml file to generate boilerplate validation where possible(non-null, max-length, etc.) while allowing developers to add custom validation easily
  3. Integrate with the Asp.Net validation stack to automatically generate validator controls based on rules in the business layers
  4. Expose an IsValid() method to test validation before SubmitChanges is called

I’ve had the fortunate opportunity to work with someone very smart who implemented just such a solution in .net 2.0 using NHibernate and codesmith. While my solution differs in details from his, this is really another implementation of his ideas.

First some class definitions:

public abstract class RuleBase<T, PropertyType> : IRuleBase
{
	public Type TargetType { get; private set; }
	public String TargetProperty { get; private set; }
	public PropertyInfo PropertyInfo { get; private set; }
	protected abstract String ErrorMessage { get; }
	protected abstract String UIErrorMessage { get; }
	public abstract BaseValidator GetValidator(ClientScriptManager clientScriptManager);

	protected PropertyType GetValue(Object instance)
	{
		return (PropertyType)PropertyInfo.GetValue(instance, null);
	}

	public abstract Boolean IsValid(Object instance, System.Data.Linq.ChangeAction action);

	public RuleBase(String targetProperty)
	{
		if (targetProperty == null)
		{
			throw new ArgumentNullException("targetProperty");
		}
		TargetType = typeof(T);
		TargetProperty = targetProperty;
		PropertyInfo = TargetType.GetProperty(TargetProperty);
		if (PropertyInfo == null)
		{
			throw new ArgumentException(String.Format("Type {0} does not contain a property named {1}", TargetType.FullName, targetProperty), "targetProperty");
		}
	}

	public BrokenRule BrokenRule
	{
		get
		{
			return new BrokenRule(ErrorMessage, UIErrorMessage, TargetType, TargetProperty);
		}
	}
}

public class BrokenRule
{
	public String ErrorMessage { get; private set; }
	public String UIErrorMessage { get; private set; }
	public Type TargetType { get; private set; }
	public String TargetProperty { get; private set; }
}

public class BrokenRulesException : Exception
{
...
	private List<BrokenRule> _brokenRulesInternal;
	private List<BrokenRule> BrokenRulesInternal
	{
		get
		{
			if (_brokenRulesInternal == null)
			{
				_brokenRulesInternal = new List();
			}
			return _brokenRulesInternal;
		}
	}

	public IEnumerable<BrokenRule> BrokenRules
	{
		get
		{
			return BrokenRulesInternal;
		}
	}

	public override string ToString()
	{
		var output = new StringBuilder();
		foreach (var brokenRule in BrokenRules)
		{
			output.AppendLine(brokenRule.ErrorMessage);
		}
		output.AppendLine(base.ToString());
		return output.ToString();
	}

}

An inheritor of RuleBase validates a property of a type. It must implement a method to validate the property, optionally register a validator for the UI, and exposes messages for the UI and business layers when validation fails.

A BrokenRule encapsulates the information describing a rule that has failed validation.

A BrokenRuleException is an exception that contains a list of BrokenRule objects. Each broken rule describes the type and property it validated and exposes an error message for both the business and UI layers.

Assuming we have a way to retrieve a list of rules for a given type, we can then implement Validate() and the L2Sql partial method OnValidate(ChangeAction action) as:

public partial class Customer
{
	public IEnumerable<BrokenRule> Validate()
	{
		var action = System.Data.Linq.ChangeAction.None;
		return ValidationRules.Validate<Customer>(this, action);
	}

	partial void OnValidate(System.Data.Linq.ChangeAction action)
	{
		var brokenRules = ValidationRules.Validate<Customer>(this, action);
		if (brokenRules.Count() > 0)
		{
			throw new BrokenRulesException(this.GetType(), brokenRules);
		}
	}
}

public abstract partial class ValidationRules
{
	private static Dictionary<Type, Dictionary<String, List<IRuleBase>>> _rules = new Dictionary<Type, Dictionary<String, List<IRuleBase>>>();

	public static IEnumerable<IRuleBase> GetRules<T>() where T : class
	{
		var ruleLists = new Dictionary<String, List<IRuleBase>>();
		var rules = new List<IRuleBase>();
		var targetInstanceType = typeof(T);
		if (Rules.ContainsKey(targetInstanceType))
		{
			ruleLists = Rules[targetInstanceType];
		}
		else
		{
			return rules;
		}

		foreach (var prop in ruleLists)
		{
			rules.AddRange(prop.Value);
		}
		return rules;
	}

	public static IEnumerable<BrokenRule> Validate<T>(Object instance, System.Data.Linq.ChangeAction action) where T : class
	{
		if (instance == null) {
			throw new ArgumentNullException("instance");
		}
		var brokenRules = GetRules<T>().Where(x => !x.IsValid(instance, action)).Select(x=>x.BrokenRule);
		return brokenRules;
	}
}

ValidationRules acts as a singleton object that contains a listing of Rules for each property for each Type validated. When an entity needs to validate itself, a list of rules is built across all the properties in that type and each rule’s validate method is called.

The actual implementation is a little trickier than this because of the extensive use of static methods and inheritance, but that’s the general idea.

The next step was to try and generate as many rules as possible – the easy ones that don’t involve any business specific logic: maxLenth, required, and date range validation.

The dbml file contains a lot of information about each column’s type and size. We can tap into this with l2st4 and use a custom T4 template to generate rules for each property. Here’s the heart of the T4 template:

<#+
		foreach(Table table in data.Tables)
		{
			foreach(TableClass class1 in table.Classes)
			{
				foreach(var column in class1.Columns)
				{
					if (!column.CanBeNull && !column.Type.IsPrimitive && !column.Type.Equals(typeof(Decimal)))
					{
#>
			AddNewRule(new Required<<#= class1.Name#>, <#=column.Type.Name#>>("<#=column.Name#>"));
<#+
					}
					if (typeof(String).Equals(column.Type))
					{
						var maxLength = String.Empty;
						var matches = System.Text.RegularExpressions.Regex.Matches(column.DbType,"(varchar|nchar|char|nvarchar)\\s*\\(\\s*([0-9]+)\\s*\\)", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
						if (matches.Count > 0)
						{
							maxLength = matches[0].Groups[2].ToString();
						}
						if (maxLength.Length > 0)
						{
#>
			AddNewRule(new MaxStringLength<<#= class1.Name#>>("<#=column.Name#>", <#=maxLength#>));
<#+
						}//maxLength.Length > 0
					} //column Type is String
					if (typeof(DateTime).Equals(column.Type))
					{
#>
			AddNewRule(new SqlDateTime<<#= class1.Name#>>("<#=column.Name#>"));
<#+
					}
					if (typeof(DateTime?).Equals(column.Type))
					{
#>
			AddNewRule(new SqlDateTimeNullable<<#= class1.Name#>>("<#=column.Name#>"));
<#+
					}
				}
#>

For each type, for each column, we generate a rule to ensure that the non-nullable columns are required, that string values are restricted in size and that datetime columns are valid Sql datetimes.

Once we have our dictionary of rules, generating client side validation is relatively straightforward, if not a little limited:

public class CodeGenFieldValidator : System.Web.UI.WebControls.WebControl
{

	protected override void CreateChildControls()
	{
		this.Controls.Clear();
		if (_type != null && this.PropertyName.Length > 0 && this.ControlToValidate.Length > 0)
		{
			var property = _type.GetProperty(this.PropertyName);
			if (property == null)
			{
				throw new InvalidOperationException(String.Format("Cannot find property '{0}' to validate in type '{1}'", PropertyName, _type.FullName));
			}
			var rules = _type.GetMethod("GetRules").Invoke(null, new object[] { PropertyName }) as IEnumerable;
			if (rules.Count() > 0)
			{
				foreach (var rule in rules)
				{
					System.Web.UI.WebControls.BaseValidator validationControl = rule.GetValidator(this.Page.ClientScript);
					if (validationControl != null)
					{
						var spanContainingValidator = new Label { CssClass = ValidationItemCssClass };
						validationControl.ControlToValidate = this.ControlToValidate;
						validationControl.Display = this.Display;
						validationControl.ErrorMessage = rule.BrokenRule.UIErrorMessage;
						validationControl.EnableClientScript = this.EnableClientScript;
						validationControl.Enabled = this.Enabled;
						validationControl.SetFocusOnError = this.SetFocusOnError;
						validationControl.ForeColor = this.ForeColor;
						if (!String.IsNullOrEmpty(ValidationGroup))
						{
							validationControl.ValidationGroup = ValidationGroup;
						}
						validationControl.EnableClientScript = true;
						spanContainingValidator.Controls.Add(validationControl);
						//this.Controls.Add(spanContainingValidator);
						this.Controls.Add(validationControl);
					}
				}
			}
		}
		else
		{
			base.ClearChildViewState();
		}
	}

	private Type _type;
	/// 
	/// Gets or sets the name of the class that the control validates against.
	/// Return Value: A partially or fully qualified class name that identifies the type of the object that the type to validate against represents.
	/// The default is an empty string ("").
	/// 
	[DefaultValue("")]
	public string TypeName
	{
		get
		{
			object typeName = this.ViewState["TypeName"];
			if (typeName != null)
			{
				return (string)typeName;
			}
			return string.Empty;
		}
		set
		{
			this.ViewState["TypeName"] = value;
			if (!String.IsNullOrEmpty(value))
			{
				_type = Type.GetType(value, false, true);
				if (_type == null)
				{
					throw new InvalidOperationException("Cannot set TypeName to Type it cannot find");
				}
			}
			else
			{
				_type = null;
			}
		}
	}

	/// 
	/// Gets or sets the name of the property in type to validate against.
	/// The default is an empty string ("").
	/// 
	[DefaultValue("")]
	public string PropertyName
	{
		get
		{
			object propertyName = this.ViewState["PropertyName"];
			if (propertyName != null)
			{
				return (string)propertyName;
			}
			return string.Empty;
		}
		set
		{
			this.ViewState["PropertyName"] = value;
		}
	}

	public CodeGenFieldValidator()
	{
		this.ForeColor = Color.Red;
	}

	/// 
	/// Gets or sets the input control to validate.
	/// Return Value: The input control to validate. The default value is Empty, which indicates that this property is not set.
	/// 
	[TypeConverter(typeof(ValidatedControlConverter)), Themeable(false), DefaultValue(""), IDReferenceProperty]
	public string ControlToValidate
	{
		get
		{
			object controlToValidate = this.ViewState["ControlToValidate"];
			if (controlToValidate != null)
			{
				return (string)controlToValidate;
			}
			return string.Empty;
		}
		set
		{
			this.ViewState["ControlToValidate"] = value;
		}
	}
...

and used like so:

<asp:TextBox ID="tbCompanyName" runat="server" Text='<%# Bind("CompanyName") %>'>
</asp:TextBox>
<cc:CodeGenFieldValidator
TypeName="LinqToSqlCodeGenValidationSample.Customer, LinqToSqlCodeGenValidationSample"
PropertyName="CompanyName"
ControlToValidate="tbCompanyName"
runat="server"
ID="fvCompanyName"
Display="Dynamic"
ValidationItemCssClass="ValidationItem"
CssClass="ValidationGroup"></cc:CodeGenFieldValidator>

There’s a lot more logic built into the generation of the ValidationRules object and the specific rules themselves than I’ve shown here.
You can find a sample with of all this using the northwind database and a variety of additional validators (numeric, email, mixed case, etc.) here.

2 Comments »

2 Responses to “UI and Business Validation with Linq to SQL and T4”

  1. Sazzad on 15 Dec 2009 at 6:36 am #

    great article. any idea implementing IDataErrorInfo as well for the generated entities. This will help a lot on WPF apps as most of binding will understand this interface to provide automatic error feedback. thx.

  2. phil on 19 Dec 2009 at 12:50 pm #

    that’s a great idea! I’ve updated the t4 codegeneration template and added a small unit test. check out the result here:
    http://www.blogfor.net/wp-content/uploads/2009/12/LinqToSqlCodeGenValidationSample.zip

Trackback URI | Comments RSS

Leave a Reply

« | »