c# - Why are sealed attributes slower to fetch than unsealed with GetCustomAttribute<T>()? -
i running code through ndepend tonight , had rule violation has me bit perplexed. rule quotes following msdn:
the .net framework class library provides methods retrieving custom attributes. default, these methods search attribute inheritance hierarchy; example attribute.getcustomattribute searches specified attribute type, or attribute type extends specified attribute type. sealing attribute eliminates search through inheritance hierarchy, , can improve performance.
i have validation base class takes validation rules (attributes) , processes them ensure property on instance meets criteria. when class instanced, runs through , caches of propertyinfo's , validation attributes ran through later. cache static dictionary each subsequent instance pulls dictionary. call validateall() method on class iterates through of propertyinfo's , associated attributes , invokes validate() method on them.
i created fixture , unit test iterate on 1,000 instances. test performed validation rules being both sealed , unsealed. unsealed tests ran faster sealed, has me bit confused. @ least expected them run @ same speed, not slower.
- unsealed attributes: 41.3199ms
- sealed attributes: 42.6241ms (using
getcustomattribute(typeof(validationattribute), true)) - sealed attributes: 43.3881ms (using
getcustomattribute(typeof(validationattribute), false))
the 2 times average on 5 different tests both sealed , unsealed.
the following unit test. included initial instancing of objects in timer bulk of getcustomattribute<ivalidationrule>() calls come from.
unit test
public void validatablebase_validationperformancetest() { // arrange var fixtures = new list<validatablefixture>(); var watch = new stopwatch(); watch.start(); (int index = 0; index < 1000; index++) { var model = new validatablefixture(); model.name = "testname"; model.password = "pass"; model.passwordconfirmation = "pass"; fixtures.add(model); } // act foreach (validatablefixture fixture in fixtures) { fixture.validateall(); } watch.stop(); debug.writeline(watch.elapsed.totalmilliseconds); } validatablefixture
public class validatablefixture : validatablebase { private const string passwordconfirmationdelegatename = "confirmpasswordsmatch"; public validatablefixture() { this.name = string.empty; this.password = string.empty; this.passwordconfirmation = string.empty; } [validatevalueisnotnullorempty(validationmessagetype = typeof(messagefixture), failuremessage = "name must set.")] public string name { get; set; } [validatevalueisnotnullorempty(validationmessagetype = typeof(messagefixture), failuremessage = "password must set.")] [validatestringisgreaterthan(greaterthanvalue = 4, validationmessagetype = typeof(messagefixture), failuremessage = "password must greater 4 characters.")] public string password { get; set; } [validatewithcustomhandler(delegatename = passwordconfirmationdelegatename, validationmessagetype = typeof(messagefixture), failuremessage = "passwords not match.")] public string passwordconfirmation { get; set; } [validationcustomhandlerdelegate(delegatename = passwordconfirmationdelegatename)] public imessage passwordconfirmationvalidation(imessage message, propertyinfo property) { return this.passwordconfirmation.equals(this.password) ? null : message; } } lastly, base class handles of validation.
validatablebase
public class validatablebase : ivalidatable { private static readonly dictionary<type, dictionary<propertyinfo, ienumerable<ivalidationrule>>> propertyvalidationcache = new dictionary<type, dictionary<propertyinfo, ienumerable<ivalidationrule>>>(); private dictionary<string, icollection<imessage>> validationmessages; public validatablebase() { this.validationmessages = new dictionary<string, icollection<imessage>>(); this.setupvalidation(); } public event eventhandler<validationchangedeventargs> validationchanged; public void addvalidationmessage(imessage message, string property) { if (string.isnullorempty(property)) { throw new argumentoutofrangeexception("property", "you must supply property name when adding new validation message instance."); } // if key not exist, create one. if (!this.validationmessages.containskey(property)) { this.validationmessages[property] = new list<imessage>(); } if (this.validationmessages[property].any(msg => msg.message.equals(message.message) || msg == message)) { return; } this.validationmessages[property].add(message); } public void removevalidationmessages() { foreach (keyvaluepair<string, icollection<imessage>> pair in this.validationmessages) { pair.value.clear(); // publish our new validation collection property. this.onvalidationchanged(new validationchangedeventargs(pair.key, this.validationmessages[pair.key])); } } public void removevalidationmessages(string property) { if (!string.isnullorempty(property) && this.validationmessages.containskey(property)) { // remove validation messages property if message isn't specified. this.validationmessages[property].clear(); this.onvalidationchanged(new validationchangedeventargs(property, this.validationmessages[property])); } } public void removevalidationmessage(imessage message, string property) { if (string.isnullorempty(property)) { return; } if (!this.validationmessages.containskey(property)) { return; } if (this.validationmessages[property].any(msg => msg == message || msg.message.equals(message.message))) { // remove error key's collection. this.validationmessages[property].remove( this.validationmessages[property].firstordefault(msg => msg == message || msg.message.equals(message.message))); this.onvalidationchanged(new validationchangedeventargs(property, this.validationmessages[property])); } } public bool hasvalidationmessages(string property = "") { if (string.isnullorempty(property) || !this.validationmessages.containskey(property)) { return this.validationmessages.values.any(collection => collection.any()); } return this.validationmessages.containskey(property) && this.validationmessages[property].any(); } public bool hasvalidationmessages(type messagetype, string property = "") { if (string.isnullorempty(property) || !this.validationmessages.containskey(property)) { return this.validationmessages.values.any(collection => collection.any(item => item.gettype() == messagetype)); } return this.validationmessages.containskey(property) && this.validationmessages[property].any(collection => collection.gettype() == messagetype); } public bool hasvalidationmessages<tmessage>(string property = "") tmessage : imessage, new() { if (string.isnullorempty(property) || !this.validationmessages.containskey(property)) { return this.validationmessages.values.any(collection => collection.any(item => item tmessage)); } return this.validationmessages.containskey(property) && this.validationmessages[property].any(message => message tmessage); } public dictionary<string, ienumerable<imessage>> getvalidationmessages() { var messages = new dictionary<string, ienumerable<imessage>>(); // have iterate on collection in order conver messages collection // icollection type ienumerable type. foreach (keyvaluepair<string, icollection<imessage>> pair in this.validationmessages) { messages.add(pair.key, pair.value); } return messages; } public ienumerable<imessage> getvalidationmessages(string property) { if (this.validationmessages.containskey(property)) { return this.validationmessages[property].toarray(); } // if no validation messages exist, return empty collection. return new collection<imessage>(); } public virtual void validateall() { this.removevalidationmessages(); dictionary<propertyinfo, ienumerable<ivalidationrule>> cache = propertyvalidationcache[this.gettype()]; foreach (keyvaluepair<propertyinfo, ienumerable<ivalidationrule>> pair in cache) { foreach (ivalidationrule rule in pair.value) { this.performvalidation(rule, pair.key); } // publish our new validation collection property. this.onvalidationchanged(new validationchangedeventargs(pair.key.name, this.validationmessages[pair.key.name])); } } public void validateproperty(string propertyname = "") { // if no property provided, assume validate everything. if (string.isnullorempty(propertyname)) { this.validateall(); return; } this.removevalidationmessages(propertyname); var cache = validatablebase.propertyvalidationcache[this.gettype()]; propertyinfo property = cache.keys.firstordefault(p => p.name.equals(propertyname)); foreach (ivalidationrule rule in cache[property]) { this.performvalidation(rule, property); } this.onvalidationchanged(new validationchangedeventargs(propertyname, this.validationmessages[propertyname])); } public imessage validateproperty(func<bool> validationdelegate, imessage failuremessage, string propertyname, ivalidatable validationproxy = null) { if (validationproxy != null) { return validationproxy.validateproperty(validationdelegate, failuremessage, propertyname); } bool passedvalidation = validationdelegate(); if (!passedvalidation) { this.addvalidationmessage(failuremessage, propertyname); } else { this.removevalidationmessage(failuremessage, propertyname); } return !passedvalidation ? failuremessage : null; } public void refreshvalidation(string property) { if (!string.isnullorempty(property) && this.hasvalidationmessages(property)) { this.validateproperty(property); } } public void performvalidation(ivalidationrule rule, string property, ivalidatable validationproxy = null) { propertyinfo propertyinfo = null; if (string.isnullorempty(property)) { throw new argumentnullexception("performvalidation requires registered property specified."); } else { propertyinfo = validatablebase.propertyvalidationcache[this.gettype()] .firstordefault(kv => kv.key.name.equals(property)).key; } if (propertyinfo == null) { throw new argumentnullexception("performvalidation requires registered property specified."); } if (validationproxy != null && validationproxy ivalidatable) { var proxy = validationproxy ivalidatable; proxy.performvalidation(rule, propertyinfo.name); } else { imessage result = rule.validate(propertyinfo, this); if (result != null) { this.addvalidationmessage(result, propertyinfo.name); } } } protected virtual void onvalidationchanged(validationchangedeventargs args) { eventhandler<validationchangedeventargs> handler = this.validationchanged; if (handler == null) { return; } handler(this, args); } private void registerproperty(params string[] propertyname) { foreach (string property in propertyname) { if (!this.validationmessages.containskey(property)) { this.validationmessages[property] = new list<imessage>(); } } } private void performvalidation(ivalidationrule rule, propertyinfo property, ivalidatable validationproxy = null) { if (validationproxy != null && validationproxy validatablebase) { var proxy = validationproxy validatablebase; proxy.performvalidation(rule, property); } imessage result = null; try { result = rule.validate(property, this); } catch (exception) { throw; } if (result != null) { this.addvalidationmessage(result, property.name); } } private void setupvalidation() { // instance cache of property info's , validation rules. if other type instanced matches ours, // won't need use reflection obtain it's members again. hit cache. var cache = new dictionary<propertyinfo, ienumerable<ivalidationrule>>(); if (!validatablebase.propertyvalidationcache.containskey(this.gettype())) { ienumerable<propertyinfo> propertiestovalidate = this.gettype().getproperties() .where(p => p.getcustomattributes(typeof(validationattribute), true).any()); // loop through property info's , build collection of validation rules each property. foreach (propertyinfo property in propertiestovalidate) { ienumerable<validationattribute> rules = property .getcustomattributes(typeof(validationattribute), true) ienumerable<validationattribute>; cache.add(property, rules); } validatablebase.propertyvalidationcache[this.gettype()] = new dictionary<propertyinfo, ienumerable<ivalidationrule>>(cache); } else { cache = validatablebase.propertyvalidationcache[this.gettype()]; } // register each property instance once done caching. foreach (propertyinfo property in cache.keys) { this.registerproperty(property.name); } } } the majority of attribute rules don't use reflection fetch additional validation attributes exception of validatewithcustomhandler does.
public sealed class validatewithcustomhandlerattribute : validationattribute { public string delegatename { get; set; } public override imessage validate(system.reflection.propertyinfo property, ivalidatable sender) { if (!this.canvalidate(sender)) { return null; } // create instance of our validation message , return if there not delegate specified. imessage validationmessage = activator.createinstance(this.validationmessagetype, this.failuremessage) imessage; if (string.isnullorempty(this.delegatename)) { return validationmessage; } // find our delegate method. ienumerable<methodinfo> validationmethods = sender .gettype() .getmethods(bindingflags.public | bindingflags.instance | bindingflags.nonpublic) .where(m => m.getcustomattributes(typeof(validationcustomhandlerdelegate), true).any()); methodinfo validationdelegate = validationmethods .firstordefault(m => m.getcustomattributes(typeof(validationcustomhandlerdelegate), true) .firstordefault(del => (del validationcustomhandlerdelegate).delegatename == this.delegatename) != null); // attempt invoke our delegate method. object result = null; try { result = validationdelegate.invoke(sender, new object[] { validationmessage, property }); } catch (exception) { throw; } // return results of delegate method. if (result != null && result imessage) { return result imessage; } else if (result == null) { return null; } return validationmessage; } } if sealed attribute supposed quicker, why unit tests 10+% slower on average? usage in base class affecting way attributes fetched while sealed?
Comments
Post a Comment