My work to rewrite Fluent NHibernate using a semantic model continues at a reasonable pace. I will admit there is some unconventional stuff in the code base, and much of it deserves explanation. Today I would like to explain AttributeStore<T> - what it is for and how it works. Lets start by taking a look at the model class for the contents of a collection with OneToMany semantics:
public class OneToManyMapping : MappingBase, ICollectionContentsMapping { private readonly AttributeStore<OneToManyMapping> _attributes; public OneToManyMapping() { _attributes = new AttributeStore<OneToManyMapping>(); _attributes.SetDefault(x => x.ExceptionOnNotFound, true); } public AttributeStore<OneToManyMapping> Attributes { get { return _attributes; } } public string ClassName { get { return _attributes.Get(x => x.ClassName); } set { _attributes.Set(x => x.ClassName, value); } } public bool ExceptionOnNotFound { get { return _attributes.Get(x => x.ExceptionOnNotFound); } set { _attributes.Set(x => x.ExceptionOnNotFound, value); } } }
As you can see, I am doing something unusual in some of the getters and setters. Basically, I am storing the values of certain properties in a dictionary, and these values are keyed on the name of the property you use to access them. Its very easy to add new attributes, I use this Resharper live template:
public $ReturnType$ $PropertyName$ { get { return $_attributes$.Get(x => x.$PropertyName$); } set { $_attributes$.Set(x => x.$PropertyName$, value); } }
This approach allows me to write code that asks questions about properties beyond simply “what is the value?”. Here is an example from the NamingConvention class:
public override void ProcessOneToMany(OneToManyMapping oneToManyMapping) { if (!oneToManyMapping.Attributes.IsSpecified(x => x.ClassName)) { if (oneToManyMapping.ChildType == null) throw new ConventionException("Cannot apply the naming convention. No type specified.", oneToManyMapping); oneToManyMapping.ClassName = DetermineNameFromType(oneToManyMapping.ChildType); } }This convention walks the mapping model, naming mappings based on the assigned Type or MemberInfo. In this example, if the ClassName property for the OneToManyMapping hasn’t been specified explicitly, then the NamingConvention uses the ChildType property to set the ClassName. If the user has set the ClassName themselves, then I don’t want the NamingConvention to overwrite that, so I ask the question “Is the ClassName property specified?”. If the ClassName property was just implemented as an autoproperty, I would probably have to check if it was null. But what if the property was a bool? Make it a nullable bool? Then I would have nasty if(blah.HasValue) code in various places. Yuck!
Here is another example, this time from the class that creates a HbmOneToMany instance based on my OneToManyMapping:
public override void ProcessOneToMany(OneToManyMapping oneToManyMapping) { _hbmOneToMany = new HbmOneToMany(); _hbmOneToMany.@class = oneToManyMapping.ClassName; if(oneToManyMapping.Attributes.IsSpecified(x => x.ExceptionOnNotFound)) { _hbmOneToMany.SetNotFound(oneToManyMapping.ExceptionOnNotFound); } }
This code always sets the @class field on the HbmOneToMany, but it won’t always call SetNotFound. It only calls SetNotFound if the the ExceptionOnNotFound property was specified. The point of this behaviour is to only generate the xml the user desires. It is not mandatory to set the not-found attribute on a one-to-many element, so why write it if the user hasn’t specified it?
As well as being able to ask questions about the properties, I also wanted a convenient way to copy them. The next code sample is the code for OneToManyPart. This class is part of the fluent interface for FluentNHibernate. It builds up information on the collection being mapped, and builds the appropriate collection when ResolveCollectionMapping() is called (obviously the IsInverse property is the only value copied at the moment, but that will change as the supported functionality grows):
public class OneToManyPart<PARENT, CHILD> : IDeferredCollectionMapping { private readonly PropertyInfo _info; private readonly AttributeStore<ICollectionMapping> _attributes; private Func<ICollectionMapping> _collectionBuilder; public OneToManyPart(PropertyInfo info) { _info = info; _attributes = new AttributeStore<ICollectionMapping>(); AsBag(); } public OneToManyPart<PARENT, CHILD> AsBag() { _collectionBuilder = () => new BagMapping(); return this; } public OneToManyPart<PARENT, CHILD> AsSet() { _collectionBuilder = () => new SetMapping(); return this; } public OneToManyPart<PARENT, CHILD> IsInverse() { _attributes.Set(x => x.IsInverse, true); return this; } ICollectionMapping IDeferredCollectionMapping.ResolveCollectionMapping() { var collection = _collectionBuilder(); _attributes.CopyTo(collection.Attributes); collection.PropertyInfo = _info; collection.Key = new KeyMapping(); collection.Contents = new OneToManyMapping {ChildType = typeof (CHILD)}; return collection; } }
The relevant lines are at the beginning of ResolveCollectionMapping(). Once the collection instance is created, the attributes collected in the _attributes field are copied to the AttributeStore for the new collection instance.
Well that is probably enough examples of why I am using this pattern. Now I want to run through the implementation. Lets start with AttributeStore<T>:
public class AttributeStore<T> { private readonly AttributeStore _store; public AttributeStore() : this(new AttributeStore()) { } public AttributeStore(AttributeStore store) { _store = store; } public U Get<U>(Expression<Func<T, U>> exp) { return (U)(_store[GetKey(exp)] ?? default(U)); } public void Set<U>(Expression<Func<T, U>> exp, U value) { _store[GetKey(exp)] = value; } public void SetDefault<U>(Expression<Func<T, U>> exp, U value) { _store.SetDefault(GetKey(exp), value); } public bool IsSpecified<U>(Expression<Func<T, U>> exp) { return _store.IsSpecified(GetKey(exp)); } public void CopyTo(AttributeStore<T> target) { _store.CopyTo(target._store); } private string GetKey<U>(Expression<Func<T, U>> exp) { PropertyInfo info = ReflectionHelper.GetProperty(exp); return info.Name; } }
As you can see, AttributeStore<T> is a generic wrapper for a non-generic class called AttributeStore. The purpose of AttributeStore<T> is to expose get and set methods that take a lambda, and convert that lambda into a dictionary key, and then delegate to an inner attribute store using that dictionary key. Finally, here is the code for the non-generic attribute store:
public class AttributeStore { private readonly IDictionary<string, object> _attributes; private readonly IDictionary<string, object> _defaults; public AttributeStore() { _attributes = new Dictionary<string, object>(); _defaults = new Dictionary<string, object>(); } public object this[string key] { get { if (_attributes.ContainsKey(key)) return _attributes[key]; if (_defaults.ContainsKey(key)) return _defaults[key]; return null; } set { _attributes[key] = value; } } public bool IsSpecified(string key) { return _attributes.ContainsKey(key); } public void CopyTo(AttributeStore store) { foreach (KeyValuePair<string, object> pair in _attributes) store._attributes[pair.Key] = pair.Value; } public void SetDefault(string key, object value) { _defaults[key] = value; } }
AttributeStore is just a wrapper for a couple of dictionaries, one for the values that have been specified, and one for the default values. That’s pretty much all there is to it.
I see AttributeStore<T> as a superior alternative to the weakly typed bag of attributes approach that Fluent NHibernate currently uses.There are no magic strings, and its all strongly typed. Its more powerful than just using properties with backing fields, and it requires pretty much the same amount of code. Sure, its much slower, but performance is not really a concern for Fluent NHibernate. I can see myself using this pattern on other projects.
No comments:
Post a Comment