// Copyright © 2008 John M Rusk (http://dotnet.agilekiwi.com) // // You may use this source code ("The Software") in any manner you wish, // subject to the following conditions: // // (a) The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // (b) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Reflection; using System.Text.RegularExpressions; namespace AgileKiwi.SimpleValidation { /// /// Static helper class /// public static class Validation { public static Result NotApplicable = new Result(true, string.Empty); //not applicable = not failed = passed public static Result Error(string message) { return new Result(false, message); } } /// /// Result of validation /// public class Result { internal Result(bool pass, string failureMessage) { Pass = pass; _failureMessage = failureMessage; } string _failureMessage; MethodInfo _method; /// /// Did the rule pass? /// public readonly bool Pass; /// /// If failed, what was the failure message? /// public string FailureMessage { get { return _failureMessage; } private set { _failureMessage = value; } } /// /// The method that implements the rule (set by AFTER running the rule. /// public MethodInfo RuleMethod { get { return _method; } internal set // set after running a rule, to record which method produced this result { _method = value; if(!Pass && FailureMessage == string.Empty) FailureMessage = ConvertToSentence(_method.Name); // automatically set failure text based on method name of no other failure next provided } } /// /// Breaks words (based on EachWordStartingWithCapitalLetter) and the lower-cases all but the first letter in the phrase /// string ConvertToSentence(string methodName) { string messageInWords = CamelCase.Break(methodName); char firstChar = messageInWords[0]; string caseCorrectedMessage = messageInWords.Remove(0, 1).ToLowerInvariant(); //using invariant culture to avoid any suprises when code is run with different culture from what it was tested with. If you want culture specific, you have to set your own message text return firstChar + caseCorrectedMessage; } /// /// Lets us inplicity convert bools to Results /// public static implicit operator Result(bool pass) { return new Result(pass, string.Empty); } /// /// Lets us OR results together /// public static Result operator |(Result lhs, Result rhs) { bool overallPass = lhs.Pass || rhs.Pass; string failureMessage = overallPass ? string.Empty : CombineFailureMessages(lhs.FailureMessage, rhs.FailureMessage); return new Result(overallPass, failureMessage); } static string CombineFailureMessages(string a, string b) { if(a == String.Empty) return b; else if(b == String.Empty) return a; else return a + "; " + b; } /// /// (With ) this lets us treat results like bools, and gives us a working || operator from the | that we have defined /// public static bool operator true(Result r) { return r.Pass; } public static bool operator false(Result r) { return !r.Pass; } } /// /// Breaks up CamelCased strings. /// public static class CamelCase { public static string Break(string camelString) { return _wordStarts.Replace(camelString, "$1 $2"); } private static Regex _wordStarts = new Regex("([^A-Z])([A-Z])"); } internal delegate Result ValidationDelegate(T targetObject); /// /// A simple way to run validation methods, without having to use reflection on every call /// Note that, for each class that contains validation methods, you only need to make a list of /// s ONCE (e.g. store it in a static member or other singleton somewhere), /// then you can run the rules on any INSTANCE of that class just by passing the instance into the /// method. /// public class ValidationRule { public ValidationRule(MethodInfo targetMethod) { // create a "open delegate"(one that is not bound to any particular object instance). // Must be typed, via generic param in this case, so that delegates param matches type we will call it on // TODO: is there any way around that? _delegate = (ValidationDelegate) Delegate.CreateDelegate(typeof(ValidationDelegate), targetMethod); } ValidationDelegate _delegate; public MethodInfo TargetMethod { get { return _delegate.Method; } } public Result ExecuteOn(TTargetClass targetObject) { Result r = _delegate(targetObject); // call the "open delegate", supplying the isntance to invoke on (the "this parameter" in OO terms) r.RuleMethod = TargetMethod; // record, in the result, which rule was actually executed // Lets callers identify results and lets the result class default the message based on the method name return r; } } /// /// Manages validation rules for the given type. This is the public API of this validation system /// /// If this apic (a static genertic type) doesn't suit you, then you can use the ideas here to write your own... public class Validator: Validator { public static readonly Validator Default = new Validator(); public static IEnumerable ExecuteRulesOn(T targetObject) { return Default.DoExecute(targetObject); } private Validator() { List> ruleList = new List>(); foreach (MethodInfo method in typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { if (method.GetCustomAttributes(typeof(ValidateAttribute), true).Length > 0) ruleList.Add(new ValidationRule(method)); } _rules = new ReadOnlyCollection>(ruleList); } ReadOnlyCollection> _rules; IEnumerable DoExecute(T targetObject) { foreach (ValidationRule rule in _rules) yield return rule.ExecuteOn(targetObject); } public override IEnumerable ExecuteRulesOn(object targetObject) { return DoExecute((T)targetObject); } } /// /// A non-generic base class - handy for cases where you don't know (at compile time) which type you'll be validating /// public abstract class Validator { /// /// I'd suggest that you cache this in some kind of Dictionary{Type, Validator} if you want to /// make it's reflection into a one-off performance cost /// public static Validator GetInstanceFor(Type t) { // return the .Instance of the corresponding generic subtype of Validator Type genericType = typeof(Validator<>).MakeGenericType(t); FieldInfo field = genericType.GetField("Default", BindingFlags.Static | BindingFlags.Public); return (Validator)field.GetValue(null); } /// /// An untyped way to execute rules (if you don't have a /// public abstract IEnumerable ExecuteRulesOn(object targetObject); } /// /// Marks a method as a validation method /// [AttributeUsage(AttributeTargets.Method)] public class ValidateAttribute : Attribute { } }