/* Copyright 2010-2011 10gen Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Text; using System.Text.RegularExpressions; using MongoDB.Bson.IO; using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Conventions; namespace MongoDB.Bson.Serialization { /// /// Represents a mapping between a class and a BSON document. /// public abstract class BsonClassMap { #region private static fields private static List profiles = new List(); private static ConventionProfile defaultProfile = ConventionProfile.GetDefault(); private static Dictionary classMaps = new Dictionary(); private static int freezeNestingLevel = 0; private static Queue knownTypesQueue = new Queue(); #endregion #region protected fields /// /// Whether the class map has been frozen. /// protected bool frozen; // once a class map has been frozen no further changes are allowed /// /// The class map for the base class. /// protected BsonClassMap baseClassMap; // null for class object and interfaces /// /// The class that this class map is for. /// protected Type classType; /// /// A function that creates a new instance of the class. /// private Func creator; /// /// The convention profile used by this class map. /// protected ConventionProfile conventions; /// /// The discriminator value. /// protected string discriminator; /// /// Whether a discriminator is required. /// protected bool discriminatorIsRequired; /// /// Whether this class is descended from a root class. /// protected bool hasRootClass; /// /// Whether this class is a root class. /// protected bool isRootClass; /// /// Whether this class is an anonymous class. /// protected bool isAnonymous; /// /// The member map for the id property or field. /// protected BsonMemberMap idMemberMap; /// /// A list of all the member maps for this class map (including member maps for inherited properties and fields). /// protected List allMemberMaps = new List(); // includes inherited member maps /// /// A list of member maps for properties or fields declared in this class. /// protected List declaredMemberMaps = new List(); // only the members declared in this class /// /// A dictionary mapping element names to the corresponding member map. /// protected Dictionary elementDictionary = new Dictionary(); /// /// Whether to ignore extra elements during deserialization. /// protected bool ignoreExtraElements = true; /// /// The member map for the property or field (if any) used to hold any extra elements found during deserialization. /// protected BsonMemberMap extraElementsMemberMap; /// /// A list of known types derived from this class. /// protected List knownTypes = new List(); #endregion #region constructors /// /// Initializes a new instance of the BsonClassMap class. /// /// The class type. protected BsonClassMap( Type classType ) { this.classType = classType; this.conventions = LookupConventions(classType); this.discriminator = classType.Name; this.isAnonymous = IsAnonymousType(classType); } #endregion #region public properties /// /// Gets the base class map. /// public BsonClassMap BaseClassMap { get { return baseClassMap; } } /// /// Gets the class type. /// public Type ClassType { get { return classType; } } /// /// Gets the discriminator. /// public string Discriminator { get { return discriminator; } } /// /// Gets whether a discriminator is required when serializing this class. /// public bool DiscriminatorIsRequired { get { return discriminatorIsRequired; } } /// /// Gets the member map of the member used to hold extra elements. /// public BsonMemberMap ExtraElementsMemberMap { get { return extraElementsMemberMap; } } /// /// Gets whether this class has a root class ancestor. /// public bool HasRootClass { get { return hasRootClass; } } /// /// Gets the Id member map. /// public BsonMemberMap IdMemberMap { get { return idMemberMap; } } /// /// Gets whether extra elements should be ignored when deserializing. /// public bool IgnoreExtraElements { get { return ignoreExtraElements; } } /// /// Gets whether this class is anonymous. /// public bool IsAnonymous { get { return isAnonymous; } } /// /// Gets whether the class map is frozen. /// public bool IsFrozen { get { return frozen; } } /// /// Gets whether this class is a root class. /// public bool IsRootClass { get { return isRootClass; } } /// /// Gets the known types of this class. /// public IEnumerable KnownTypes { get { return knownTypes; } } /// /// Gets the member maps. /// public IEnumerable MemberMaps { get { return allMemberMaps; } } #endregion #region public static methods /// /// Gets the type of a member. /// /// The member info. /// The type of the member. public static Type GetMemberInfoType( MemberInfo memberInfo ) { if (memberInfo.MemberType == MemberTypes.Field) { return ((FieldInfo) memberInfo).FieldType; } else if (memberInfo.MemberType == MemberTypes.Property) { return ((PropertyInfo) memberInfo).PropertyType; } throw new NotSupportedException("Only field and properties are supported at this time."); } /// /// Gets a loadable type name (like AssemblyQualifiedName but shortened when possible) /// /// The type. /// The type name. public static string GetTypeNameDiscriminator( Type type ) { string typeName; if (type.IsGenericType) { var genericTypeNames = ""; foreach (var genericType in type.GetGenericArguments()) { var genericTypeName = GetTypeNameDiscriminator(genericType); if (genericTypeName.Contains(',')) { genericTypeName = "[" + genericTypeName + "]"; } if (genericTypeNames != "") { genericTypeNames += ","; } genericTypeNames += genericTypeName; } typeName = type.GetGenericTypeDefinition().FullName + "[" + genericTypeNames + "]"; } else { typeName = type.FullName; } string assemblyName = type.Assembly.FullName; Match match = Regex.Match(assemblyName, "(?[^,]+), Version=[^,]+, Culture=[^,]+, PublicKeyToken=(?[^,]+)"); if (match.Success) { var dll = match.Groups["dll"].Value; var publicKeyToken = match.Groups["token"].Value; if (dll == "mscorlib") { assemblyName = null; } else if (publicKeyToken == "null") { assemblyName = dll; } } if (assemblyName == null) { return typeName; } else { return typeName + ", " + assemblyName; } } /// /// Checks whether a class map is registered for a type. /// /// The type to check. /// True if there is a class map registered for the type. public static bool IsClassMapRegistered( Type type ) { BsonSerializer.ConfigLock.EnterReadLock(); try { return classMaps.ContainsKey(type); } finally { BsonSerializer.ConfigLock.ExitReadLock(); } } /// /// Looks up a class map (will AutoMap the class if no class map is registered). /// /// The class type. /// The class map. public static BsonClassMap LookupClassMap( Type classType ) { BsonSerializer.ConfigLock.EnterReadLock(); try { BsonClassMap classMap; if (classMaps.TryGetValue(classType, out classMap)) { if (classMap.IsFrozen) { return classMap; } } } finally { BsonSerializer.ConfigLock.ExitReadLock(); } BsonSerializer.ConfigLock.EnterWriteLock(); try { BsonClassMap classMap; if (!classMaps.TryGetValue(classType, out classMap)) { // automatically create a classMap for classType and register it var classMapDefinition = typeof(BsonClassMap<>); var classMapType = classMapDefinition.MakeGenericType(classType); classMap = (BsonClassMap) Activator.CreateInstance(classMapType); classMap.AutoMap(); RegisterClassMap(classMap); } return classMap.Freeze(); } finally { BsonSerializer.ConfigLock.ExitWriteLock(); } } /// /// Looks up the conventions profile for a type. /// /// The type. /// The conventions profile for that type. public static ConventionProfile LookupConventions( Type type ) { for (int i = 0; i < profiles.Count; i++) { if (profiles[i].Filter(type)) { return profiles[i].Profile; } } return defaultProfile; } /// /// Creates and registers a class map. /// /// The class. /// The class map. public static BsonClassMap RegisterClassMap() { return RegisterClassMap(cm => { cm.AutoMap(); }); } /// /// Creates and registers a class map. /// /// The class. /// The class map initializer. /// The class map. public static BsonClassMap RegisterClassMap( Action> classMapInitializer ) { var classMap = new BsonClassMap(classMapInitializer); RegisterClassMap(classMap); return classMap; } /// /// Registers a class map. /// /// The class map. public static void RegisterClassMap( BsonClassMap classMap ) { BsonSerializer.ConfigLock.EnterWriteLock(); try { // note: class maps can NOT be replaced (because derived classes refer to existing instance) classMaps.Add(classMap.ClassType, classMap); BsonDefaultSerializer.RegisterDiscriminator(classMap.ClassType, classMap.Discriminator); } finally { BsonSerializer.ConfigLock.ExitWriteLock(); } } /// /// Registers a conventions profile. /// /// The conventions profile. /// The filter function that determines which types this profile applies to. public static void RegisterConventions( ConventionProfile conventions, Func filter ) { conventions.Merge(defaultProfile); // make sure all conventions exists var filtered = new FilteredConventionProfile { Filter = filter, Profile = conventions }; // add new conventions to the front of the list profiles.Insert(0, filtered); } #endregion #region public methods /// /// Automaps the class. /// public void AutoMap() { if (frozen) { ThrowFrozenException(); } AutoMapClass(); } /// /// Creates an instance of the class. /// /// An object. public object CreateInstance() { if (!frozen) { ThrowNotFrozenException(); } var creator = GetCreator(); return creator.Invoke(); } /// /// Freezes the class map. /// /// The frozen class map. public BsonClassMap Freeze() { BsonSerializer.ConfigLock.EnterReadLock(); try { if (frozen) { return this; } } finally { BsonSerializer.ConfigLock.ExitReadLock(); } BsonSerializer.ConfigLock.EnterWriteLock(); try { if (!frozen) { freezeNestingLevel++; try { var baseType = classType.BaseType; if (baseType != null) { baseClassMap = LookupClassMap(baseType); discriminatorIsRequired |= baseClassMap.discriminatorIsRequired; hasRootClass |= (isRootClass || baseClassMap.HasRootClass); allMemberMaps.AddRange(baseClassMap.MemberMaps); } allMemberMaps.AddRange(declaredMemberMaps); if (idMemberMap == null) { // see if we can inherit the idMemberMap from our base class if (baseClassMap != null) { idMemberMap = baseClassMap.IdMemberMap; } // if our base class did not have an idMemberMap maybe we have one? if (idMemberMap == null) { var memberName = conventions.IdMemberConvention.FindIdMember(classType); if (memberName != null) { var memberMap = GetMemberMap(memberName); if (memberMap != null) { SetIdMember(memberMap); } } } } if (extraElementsMemberMap == null) { // see if we can inherit the extraElementsMemberMap from our base class if (baseClassMap != null) { extraElementsMemberMap = baseClassMap.ExtraElementsMemberMap; } // if our base class did not have an extraElementsMemberMap maybe we have one? if (extraElementsMemberMap == null) { var memberName = conventions.ExtraElementsMemberConvention.FindExtraElementsMember(classType); if (memberName != null) { var memberMap = GetMemberMap(memberName); if (memberMap != null) { SetExtraElementsMember(memberMap); } } } } foreach (var memberMap in allMemberMaps) { BsonMemberMap conflictingMemberMap; if (!elementDictionary.TryGetValue(memberMap.ElementName, out conflictingMemberMap)) { elementDictionary.Add(memberMap.ElementName, memberMap); } else { var fieldOrProperty = (memberMap.MemberInfo.MemberType == MemberTypes.Field) ? "field" : "property"; var conflictingFieldOrProperty = (conflictingMemberMap.MemberInfo.MemberType == MemberTypes.Field) ? "field" : "property"; var conflictingType = conflictingMemberMap.MemberInfo.DeclaringType; string message; if (conflictingType == classType) { message = string.Format( "The {0} '{1}' of type '{2}' cannot use element name '{3}' because it is already being used by {4} '{5}'.", fieldOrProperty, memberMap.MemberName, classType.FullName, memberMap.ElementName, conflictingFieldOrProperty, conflictingMemberMap.MemberName ); } else { message = string.Format( "The {0} '{1}' of type '{2}' cannot use element name '{3}' because it is already being used by {4} '{5}' of type '{6}'.", fieldOrProperty, memberMap.MemberName, classType.FullName, memberMap.ElementName, conflictingFieldOrProperty, conflictingMemberMap.MemberName, conflictingType.FullName ); } throw new BsonSerializationException(message); } } // mark this classMap frozen before we start working on knownTypes // because we might get back to this same classMap while processing knownTypes frozen = true; // use a queue to postpone processing of known types until we get back to the first level call to Freeze // this avoids infinite recursion when going back down the inheritance tree while processing known types foreach (var knownType in knownTypes) { knownTypesQueue.Enqueue(knownType); } // if we are back to the first level go ahead and process any queued known types if (freezeNestingLevel == 1) { while (knownTypesQueue.Count != 0) { var knownType = knownTypesQueue.Dequeue(); LookupClassMap(knownType); // will AutoMap and/or Freeze knownType if necessary } } } finally { freezeNestingLevel--; } } } finally { BsonSerializer.ConfigLock.ExitWriteLock(); } return this; } /// /// Gets a member map. /// /// The member name. /// The member map. public BsonMemberMap GetMemberMap( string memberName ) { // can be called whether frozen or not return declaredMemberMaps.Find(m => m.MemberName == memberName); } /// /// Gets the member map for a BSON element. /// /// The name of the element. /// The member map. public BsonMemberMap GetMemberMapForElement( string elementName ) { if (!frozen) { ThrowNotFrozenException(); } BsonMemberMap memberMap; elementDictionary.TryGetValue(elementName, out memberMap); return memberMap; } /// /// Creates a member map for the extra elements field and adds it to the class map. /// /// The name of the extra elements field. /// The member map (so method calls can be chained). public BsonMemberMap MapExtraElementsField( string fieldName ) { if (frozen) { ThrowFrozenException(); } var fieldMap = MapField(fieldName); SetExtraElementsMember(fieldMap); return fieldMap; } /// /// Creates a member map for the extra elements member and adds it to the class map. /// /// The member info for the extra elements member. /// The member map (so method calls can be chained). public BsonMemberMap MapExtraElementsMember( MemberInfo memberInfo ) { if (frozen) { ThrowFrozenException(); } var memberMap = MapMember(memberInfo); SetExtraElementsMember(memberMap); return memberMap; } /// /// Creates a member map for the extra elements property and adds it to the class map. /// /// The name of the property. /// The member map (so method calls can be chained). public BsonMemberMap MapExtraElementsProperty( string propertyName ) { if (frozen) { ThrowFrozenException(); } var propertyMap = MapProperty(propertyName); SetExtraElementsMember(propertyMap); return propertyMap; } /// /// Creates a member map for a field and adds it to the class map. /// /// The name of the field. /// The member map (so method calls can be chained). public BsonMemberMap MapField( string fieldName ) { if (frozen) { ThrowFrozenException(); } var fieldInfo = classType.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); if (fieldInfo == null) { var message = string.Format("The class '{0}' does not have a field named '{1}'.", classType.FullName, fieldName); throw new BsonSerializationException(message); } return MapMember(fieldInfo); } /// /// Creates a member map for the Id field and adds it to the class map. /// /// The name of the Id field. /// The member map (so method calls can be chained). public BsonMemberMap MapIdField( string fieldName ) { if (frozen) { ThrowFrozenException(); } var fieldMap = MapField(fieldName); SetIdMember(fieldMap); return fieldMap; } /// /// Creates a member map for the Id member and adds it to the class map. /// /// The member info for the Id member. /// The member map (so method calls can be chained). public BsonMemberMap MapIdMember( MemberInfo memberInfo ) { if (frozen) { ThrowFrozenException(); } var memberMap = MapMember(memberInfo); SetIdMember(memberMap); return memberMap; } /// /// Creates a member map for the Id property and adds it to the class map. /// /// The name of the Id property. /// The member map (so method calls can be chained). public BsonMemberMap MapIdProperty( string propertyName ) { if (frozen) { ThrowFrozenException(); } var propertyMap = MapProperty(propertyName); SetIdMember(propertyMap); return propertyMap; } /// /// Creates a member map for a member and adds it to the class map. /// /// The member info. /// The member map (so method calls can be chained). public BsonMemberMap MapMember( MemberInfo memberInfo ) { if (frozen) { ThrowFrozenException(); } if (memberInfo == null) { throw new ArgumentNullException("memberInfo"); } if (memberInfo.DeclaringType != classType) { throw new ArgumentException("MemberInfo is not for this class."); } var memberMap = declaredMemberMaps.Find(m => m.MemberInfo == memberInfo); if (memberMap == null) { memberMap = new BsonMemberMap(memberInfo, conventions); declaredMemberMaps.Add(memberMap); } return memberMap; } /// /// Creates a member map for a property and adds it to the class map. /// /// The name of the property. /// The member map (so method calls can be chained). public BsonMemberMap MapProperty( string propertyName ) { if (frozen) { ThrowFrozenException(); } var propertyInfo = classType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); if (propertyInfo == null) { var message = string.Format("The class '{0}' does not have a property named '{1}'.", classType.FullName, propertyName); throw new BsonSerializationException(message); } return MapMember(propertyInfo); } /// /// Sets the discriminator. /// /// The discriminator. public void SetDiscriminator( string discriminator ) { if (frozen) { ThrowFrozenException(); } this.discriminator = discriminator; } /// /// Sets whether a discriminator is required when serializing this class. /// /// Whether a discriminator is required. public void SetDiscriminatorIsRequired( bool discriminatorIsRequired ) { if (frozen) { ThrowFrozenException(); } this.discriminatorIsRequired = discriminatorIsRequired; } /// /// Sets the member map of the member used to hold extra elements. /// /// The extra elements member map. public void SetExtraElementsMember( BsonMemberMap memberMap ) { if (frozen) { ThrowFrozenException(); } if (extraElementsMemberMap != null) { var message = string.Format("Class {0} already has an extra elements member.", classType.FullName); throw new InvalidOperationException(message); } if (!declaredMemberMaps.Contains(memberMap)) { throw new BsonInternalException("Invalid memberMap."); } if (memberMap.MemberType != typeof(BsonDocument)) { var message = string.Format("Type of ExtraElements member must be BsonDocument."); throw new InvalidOperationException(message); } extraElementsMemberMap = memberMap; } /// /// Sets the Id member. /// /// The Id member. public void SetIdMember( BsonMemberMap memberMap ) { if (frozen) { ThrowFrozenException(); } if (idMemberMap != null) { var message = string.Format("Class {0} already has an Id.", classType.FullName); throw new InvalidOperationException(message); } if (!declaredMemberMaps.Contains(memberMap)) { throw new BsonInternalException("Invalid memberMap."); } memberMap.SetElementName("_id"); idMemberMap = memberMap; } /// /// Sets whether extra elements should be ignored when deserializing. /// /// Whether extra elements should be ignored when deserializing. public void SetIgnoreExtraElements( bool ignoreExtraElements ) { if (frozen) { ThrowFrozenException(); } this.ignoreExtraElements = ignoreExtraElements; } /// /// Sets whether this class is a root class. /// /// Whether this class is a root class. public void SetIsRootClass( bool isRootClass ) { if (frozen) { ThrowFrozenException(); } this.isRootClass = isRootClass; } /// /// Removes the member map for a field from the class map. /// /// The name of the field. public void UnmapField( string fieldName ) { if (frozen) { ThrowFrozenException(); } var fieldInfo = classType.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); if (fieldInfo == null) { var message = string.Format("The class '{0}' does not have a field named '{1}'.", classType.FullName, fieldName); throw new BsonSerializationException(message); } UnmapMember(fieldInfo); } /// /// Removes a member map from the class map. /// /// The member info. public void UnmapMember( MemberInfo memberInfo ) { if (frozen) { ThrowFrozenException(); } if (memberInfo == null) { throw new ArgumentNullException("memberInfo"); } if (memberInfo.DeclaringType != classType) { throw new ArgumentException("MemberInfo is not for this class."); } var memberMap = declaredMemberMaps.Find(m => m.MemberInfo == memberInfo); if (memberMap != null) { declaredMemberMaps.Remove(memberMap); if (idMemberMap == memberMap) { idMemberMap = null; } if (extraElementsMemberMap == memberMap) { extraElementsMemberMap = null; } } } /// /// Removes the member map for a property from the class map. /// /// The name of the property. public void UnmapProperty( string propertyName ) { if (frozen) { ThrowFrozenException(); } var propertyInfo = classType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); if (propertyInfo == null) { var message = string.Format("The class '{0}' does not have a property named '{1}'.", classType.FullName, propertyName); throw new BsonSerializationException(message); } UnmapMember(propertyInfo); } #endregion #region private methods private void AutoMapClass() { foreach (BsonKnownTypesAttribute knownTypesAttribute in classType.GetCustomAttributes(typeof(BsonKnownTypesAttribute), false)) { foreach (var knownType in knownTypesAttribute.KnownTypes) { knownTypes.Add(knownType); // knownTypes will be processed when Freeze is called } } var discriminatorAttribute = (BsonDiscriminatorAttribute) classType.GetCustomAttributes(typeof(BsonDiscriminatorAttribute), false).FirstOrDefault(); if (discriminatorAttribute != null) { if (discriminatorAttribute.Discriminator != null) { discriminator = discriminatorAttribute.Discriminator; } discriminatorIsRequired = discriminatorAttribute.Required; isRootClass = discriminatorAttribute.RootClass; } var ignoreExtraElementsAttribute = (BsonIgnoreExtraElementsAttribute) classType.GetCustomAttributes(typeof(BsonIgnoreExtraElementsAttribute), false).FirstOrDefault(); if (ignoreExtraElementsAttribute != null) { ignoreExtraElements = ignoreExtraElementsAttribute.IgnoreExtraElements; } else { ignoreExtraElements = conventions.IgnoreExtraElementsConvention.IgnoreExtraElements(classType); } AutoMapMembers(); } private void AutoMapMembers() { // only auto map properties declared in this class (and not in base classes) var hasOrderedElements = false; foreach (var memberInfo in FindMembers()) { var memberMap = AutoMapMember(memberInfo); hasOrderedElements = hasOrderedElements || memberMap.Order != int.MaxValue; } if (hasOrderedElements) { // split out the items with a value for Order and sort them separately (because Sort is unstable, see online help) // and then concatenate any items with no value for Order at the end (in their original order) var sorted = new List(declaredMemberMaps.Where(pm => pm.Order != int.MaxValue)); var unsorted = new List(declaredMemberMaps.Where(pm => pm.Order == int.MaxValue)); sorted.Sort((x, y) => x.Order.CompareTo(y.Order)); declaredMemberMaps = sorted.Concat(unsorted).ToList(); } } private BsonMemberMap AutoMapMember( MemberInfo memberInfo ) { var memberMap = MapMember(memberInfo); memberMap.SetElementName(conventions.ElementNameConvention.GetElementName(memberInfo)); memberMap.SetIgnoreIfNull(conventions.IgnoreIfNullConvention.IgnoreIfNull(memberInfo)); memberMap.SetSerializeDefaultValue(conventions.SerializeDefaultValueConvention.SerializeDefaultValue(memberInfo)); var defaultValue = conventions.DefaultValueConvention.GetDefaultValue(memberInfo); if (defaultValue != null) { memberMap.SetDefaultValue(defaultValue); } // see if the class has a method called ShouldSerializeXyz where Xyz is the name of this member var shouldSerializeMethod = GetShouldSerializeMethod(memberInfo); if (shouldSerializeMethod != null) { memberMap.SetShouldSerializeMethod(shouldSerializeMethod); } foreach (var attribute in memberInfo.GetCustomAttributes(false)) { var defaultValueAttribute = attribute as BsonDefaultValueAttribute; if (defaultValueAttribute != null) { memberMap.SetDefaultValue(defaultValueAttribute.DefaultValue); memberMap.SetSerializeDefaultValue(defaultValueAttribute.SerializeDefaultValue); } var elementAttribute = attribute as BsonElementAttribute; if (elementAttribute != null) { memberMap.SetElementName(elementAttribute.ElementName); memberMap.SetOrder(elementAttribute.Order); continue; } var extraElementsAttribute = attribute as BsonExtraElementsAttribute; if (extraElementsAttribute != null) { SetExtraElementsMember(memberMap); continue; } var idAttribute = attribute as BsonIdAttribute; if (idAttribute != null) { memberMap.SetElementName("_id"); memberMap.SetOrder(idAttribute.Order); var idGeneratorType = idAttribute.IdGenerator; if (idGeneratorType != null) { var idGenerator = (IIdGenerator) Activator.CreateInstance(idGeneratorType); // public default constructor required memberMap.SetIdGenerator(idGenerator); } SetIdMember(memberMap); continue; } var ignoreIfNullAttribute = attribute as BsonIgnoreIfNullAttribute; if (ignoreIfNullAttribute != null) { memberMap.SetIgnoreIfNull(true); } var requiredAttribute = attribute as BsonRequiredAttribute; if (requiredAttribute != null) { memberMap.SetIsRequired(true); } // note: this handles subclasses of BsonSerializationOptionsAttribute also var serializationOptionsAttribute = attribute as BsonSerializationOptionsAttribute; if (serializationOptionsAttribute != null) { memberMap.SetSerializationOptions(serializationOptionsAttribute.GetOptions()); } } return memberMap; } private IEnumerable FindMembers() { // use a List instead of a HashSet to preserver order var memberInfos = new List( conventions.MemberFinderConvention.FindMembers(classType) ); // let other fields opt-in if they have a BsonElement attribute foreach (var fieldInfo in classType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) { var elementAttribute = (BsonElementAttribute) fieldInfo.GetCustomAttributes(typeof(BsonElementAttribute), false).FirstOrDefault(); if (elementAttribute == null || fieldInfo.IsInitOnly || fieldInfo.IsLiteral) { continue; } if(!memberInfos.Contains(fieldInfo)) { memberInfos.Add(fieldInfo); } } // let other properties opt-in if they have a BsonElement attribute foreach (var propertyInfo in classType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) { var elementAttribute = (BsonElementAttribute) propertyInfo.GetCustomAttributes(typeof(BsonElementAttribute), false).FirstOrDefault(); if (elementAttribute == null || !propertyInfo.CanRead || (!propertyInfo.CanWrite && !isAnonymous)) { continue; } if(!memberInfos.Contains(propertyInfo)) { memberInfos.Add(propertyInfo); } } foreach(var memberInfo in memberInfos) { var ignoreAttribute = (BsonIgnoreAttribute) memberInfo.GetCustomAttributes(typeof(BsonIgnoreAttribute), false).FirstOrDefault(); if (ignoreAttribute != null) { continue; // ignore this property } yield return memberInfo; } } private Func GetCreator() { if (creator == null) { Expression body; var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; var defaultConstructor = classType.GetConstructor(bindingFlags, null, new Type[0], null); if (defaultConstructor != null) { // lambdaExpression = () => (object) new TClass() body = Expression.New(defaultConstructor); } else { // lambdaExpression = () => FormatterServices.GetUninitializedObject(classType) var getUnitializedObjectMethodInfo = typeof(FormatterServices).GetMethod("GetUninitializedObject", BindingFlags.Public | BindingFlags.Static); body = Expression.Call(getUnitializedObjectMethodInfo, Expression.Constant(classType)); } var lambdaExpression = Expression.Lambda>(body); creator = lambdaExpression.Compile(); } return creator; } private Func GetShouldSerializeMethod( MemberInfo memberInfo ) { var shouldSerializeMethodName = "ShouldSerialize" + memberInfo.Name; var shouldSerializeMethodInfo = classType.GetMethod(shouldSerializeMethodName, new Type[] { }); if ( shouldSerializeMethodInfo != null && shouldSerializeMethodInfo.IsPublic && shouldSerializeMethodInfo.ReturnType == typeof(bool) ) { // lambdaExpression = (obj) => ((TClass) obj).ShouldSerializeXyz() var objParameter = Expression.Parameter(typeof(object), "obj"); var lambdaExpression = Expression.Lambda>( Expression.Call( Expression.Convert(objParameter, classType), shouldSerializeMethodInfo ), objParameter ); return lambdaExpression.Compile(); } else { return null; } } private bool IsAnonymousType( Type type ) { // don't test for too many things in case implementation details change in the future return Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false) && type.IsGenericType && type.Name.Contains("Anon"); // don't check for more than "Anon" so it works in mono also } private void ThrowFrozenException() { var message = string.Format("Class map for {0} has been frozen and no further changes are allowed.", classType.FullName); throw new InvalidOperationException(message); } private void ThrowNotFrozenException() { var message = string.Format("Class map for {0} has been not been frozen yet.", classType.FullName); throw new InvalidOperationException(message); } #endregion #region private class private class FilteredConventionProfile { public Func Filter; public ConventionProfile Profile; } #endregion } /// /// Represents a mapping between a class and a BSON document. /// /// The class. public class BsonClassMap : BsonClassMap { #region constructors /// /// Initializes a new instance of the BsonClassMap class. /// public BsonClassMap() : base(typeof(TClass)) { } /// /// Initializes a new instance of the BsonClassMap class. /// /// The class map initializer. public BsonClassMap( Action> classMapInitializer ) : base(typeof(TClass)) { classMapInitializer(this); } #endregion #region public methods /// /// Gets a member map. /// /// The member type. /// A lambda expression specifying the member. /// The member map. public BsonMemberMap GetMemberMap( Expression> memberLambda ) { var memberName = GetMemberNameFromLambda(memberLambda); return GetMemberMap(memberName); } /// /// Creates a member map for the extra elements field and adds it to the class map. /// /// The member type. /// A lambda expression specifying the extra elements field. /// The member map. public BsonMemberMap MapExtraElementsField( Expression> fieldLambda ) { var fieldMap = MapField(fieldLambda); SetExtraElementsMember(fieldMap); return fieldMap; } /// /// Creates a member map for the extra elements member and adds it to the class map. /// /// The member type. /// A lambda expression specifying the extra elements member. /// The member map. public BsonMemberMap MapExtraElementsMember( Expression> memberLambda ) { var memberMap = MapMember(memberLambda); SetExtraElementsMember(memberMap); return memberMap; } /// /// Creates a member map for the extra elements property and adds it to the class map. /// /// The member type. /// A lambda expression specifying the extra elements property. /// The member map. public BsonMemberMap MapExtraElementsProperty( Expression> propertyLambda ) { var propertyMap = MapProperty(propertyLambda); SetExtraElementsMember(propertyMap); return propertyMap; } /// /// Creates a member map for a field and adds it to the class map. /// /// The member type. /// A lambda expression specifying the field. /// The member map. public BsonMemberMap MapField( Expression> fieldLambda ) { return MapMember(fieldLambda); } /// /// Creates a member map for the Id field and adds it to the class map. /// /// The member type. /// A lambda expression specifying the Id field. /// The member map. public BsonMemberMap MapIdField( Expression> fieldLambda ) { var fieldMap = MapField(fieldLambda); SetIdMember(fieldMap); return fieldMap; } /// /// Creates a member map for the Id member and adds it to the class map. /// /// The member type. /// A lambda expression specifying the Id member. /// The member map. public BsonMemberMap MapIdMember( Expression> memberLambda ) { var memberMap = MapMember(memberLambda); SetIdMember(memberMap); return memberMap; } /// /// Creates a member map for the Id property and adds it to the class map. /// /// The member type. /// A lambda expression specifying the Id property. /// The member map. public BsonMemberMap MapIdProperty( Expression> propertyLambda ) { var propertyMap = MapProperty(propertyLambda); SetIdMember(propertyMap); return propertyMap; } /// /// Creates a member map and adds it to the class map. /// /// The member type. /// A lambda expression specifying the member. /// The member map. public BsonMemberMap MapMember( Expression> memberLambda ) { var memberInfo = GetMemberInfoFromLambda(memberLambda); return MapMember(memberInfo); } /// /// Creates a member map for the Id property and adds it to the class map. /// /// The member type. /// A lambda expression specifying the Id property. /// The member map. public BsonMemberMap MapProperty( Expression> propertyLambda ) { return MapMember(propertyLambda); } /// /// Removes the member map for a field from the class map. /// /// The member type. /// A lambda expression specifying the field. public void UnmapField( Expression> fieldLambda ) { UnmapMember(fieldLambda); } /// /// Removes a member map from the class map. /// /// The member type. /// A lambda expression specifying the member. public void UnmapMember( Expression> memberLambda ) { var memberInfo = GetMemberInfoFromLambda(memberLambda); UnmapMember(memberInfo); } /// /// Removes a member map for a property from the class map. /// /// The member type. /// A lambda expression specifying the property. public void UnmapProperty( Expression> propertyLambda ) { UnmapMember(propertyLambda); } #endregion #region private methods private MemberInfo GetMemberInfoFromLambda( Expression> memberLambda ) { var memberName = GetMemberNameFromLambda(memberLambda); return classType.GetMember(memberName).SingleOrDefault(x => x.MemberType == MemberTypes.Field || x.MemberType == MemberTypes.Property); } private string GetMemberNameFromLambda( Expression> memberLambda ) { var body = memberLambda.Body; MemberExpression memberExpression; switch (body.NodeType) { case ExpressionType.MemberAccess: memberExpression = (MemberExpression) body; break; case ExpressionType.Convert: var convertExpression = (UnaryExpression) body; memberExpression = (MemberExpression) convertExpression.Operand; break; default: throw new BsonSerializationException("Invalid propertyLambda."); } return memberExpression.Member.Name; } #endregion } }