/* 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
}
}