I needed a mapper for a project I have using Dapper. I tried Automapper and other tools that comes with Dapper (Contrib, Rainbow, etc.), but I couldn't make Automapper work properly and the rest doesn't have what I need, so I did one.
Take in mind that I'm an amateur and not native english speaker ;)
The whole thing is in GitHub, I'll write down here what I can, but it's large, it won't fit the code limit so there's the link to the whole thing.
It works and in my tests it maps each object in about 2 milliseconds... it worry me, it's too much good.
To be honest, given that I'm an amateur and after reading lots about reflection performance overhead, I was "mentally prepared" to lose performance for the sake of having it working the way I wanted and what not, so now I'm worried if I'm overlooking something and will appreciate any advice.
I'm sorry, I tried to summarize the post but I can't summarize it any more, remember english is not my native language, and again the whole thing doesn't fit here. If you want to help (thanks you) or it interests you, you can just take a peek at the Github link I provided earlier and come back to read what you need from the rest, if you need it.
Just to be sure, both in code and here I'll refer as "Dapper's dynamic
result" to the result of the query Dapper returns. I don't know if it's really suitable, but it's easier to me to name it that way.
Also, there's a general diagram I did to have things clearer myself: in google drive.
The mapper works given some static dictionaries that stores information about the types that will be mapped, using a configuration the user have to make before map any object (at the app beginning... similar to Automapper I think).
I divided the different dictionaries in different usual public
classes, so each class of the mapper can inherit only what's needed, f.i.:
public class DMStatic_Dictionary : MQBStatic_IEnumerable
{
public DMStatic_Dictionary(string[] customNamespaces = null) : base(customNamespaces)
{
InitStaticDDictionary();
}
#region fields
protected static Dictionary<Type, Dictionary<string, string[]>> _Dictionaries;
#endregion
#region helpers
private static void InitStaticDDictionary()
{
if (_Dictionaries == null)
{
lock (_LockObject)
{
if (_Dictionaries == null)
_Dictionaries = new Dictionary<Type, Dictionary<string, string[]>>();
}
}
}
#endregion
}
They store all the information, all the members, if each member is built-in, nested or a dictionary, prefixes and suffixes for searching later through the Dapper's dynamic
result, members to ignore, etc. Lets say they are just static dictionaries with the "reflection information" of the types (nested types, enumerables, dictionaries, etc.), so I don't have to spend resources to retrieve that information at run time.
MAPPERCONFIG
As I said, the dictionaries should be filled at the very start of the app. To do that I made a fluent type class called MapperConfig
.
This class do several things.
First it populates some static dictionaries through the public methods, f.i.:
public MapperConfig AddNestedProperty<T>(bool isAnInterface, string memberName) { Type destination = typeof(T); if (_OnlyConstructor.Contains(destination)) throw new CustomException_MapperConfig( @"MapperConfig.AddNestedProperty: You can't any configuration if MapOnlyConstructor have been setted."); if (_MembersCreators.ContainsKey(destination) && _MembersCreators[destination].ContainsKey(memberName)) throw new CustomException_MapperConfig( $@"MapperConfig.AddNestedProperty: One member({memberName}) can not have a creator expression AND be setted as a nested type at same type"); if (_Ignores.ContainsKey(destination) && _Ignores[destination].Contains(memberName)) throw new CustomException_MapperConfig( $@"MapperConfig.AddNestedProperty: One member({memberName}) can not be setted as nested property if it was setted for ignore."); if (isAnInterface) { lock (_LockObject) { if (!_Interfaces.ContainsKey(destination)) _Interfaces.Add(destination, new List<string>() { memberName }); else if (!_Interfaces[destination].Contains(memberName)) _Interfaces[destination].Add(memberName); } } lock (_LockObject) { if (!_NestedProperties.ContainsKey(destination)) { _NestedProperties.Add(destination, new List<string>() { memberName }); return this; } else { if (!_NestedProperties[destination].Contains(memberName)) _NestedProperties[destination].Add(memberName); return this; } } }
This public methods are how one tell the mapper how to map each type member, if it's a nested type (
AddNestedProperty
), if it's a dictionary (AddDictionary
), if it has to run someFunc
with the Dapper'sdynamic
result to create the member (AddMemberCreator
), if it has to use a constructor (AddConstructor
, works via aFunc
again), etc.
I wrote the ones I think I'll need and others more general. Some notes about them:IEnumerable
members are automated, the mapper will identify and map them automatically therefore there's no need to configure them and no method in this class for them, but I didn't find a way to automate dictionaries so I did a specific method to declare an object member as a dictionary to map it later: one need to pass the names that keys and values will have at the Dapper'sdynamic
result, so the mapper can find them at runtime. If keys or values (or both) are custom, nested types, one have to configure the member as a nested property too:.AddNestedProperty<Finca>(false, "_Cuotas") .AddDictionary<Finca>("_Cuotas", new string[2] { "cuotaId", "Cuota" })
The mapper will find what's nested (keys, values or both), map the built-in types using the name provided, ignore the name of the nested type and map the nested type recursively with a new mapper.
Other note: as you probably have noted, a single member can be declared as various things, for instance (again) if a member is of type
Dictionary<MyClass, int>
, not only it can be declared as dictionary and nested, it MUST be declared as both of them or later the mapper will try to map keys as a built-in type and throw an exception.Second it populates more static dictionaries with reflection. I didn't want to search and retrieve all member of all objects-to-map at run time, so I store every member of the objects configured in the dictionaries:
protected static Dictionary<Type, PropertyInfo[]> _QBPropertyInfos; protected static Dictionary<Type, FieldInfo[]> _QBFieldInfos;
This is done in the method
private void SetMembersInformation(Type t)
of theMapperConfig
class:private void SetMembersInformation(Type t) { if (_OnlyConstructor.Contains(t)) RemoveAllConfigExceptConstructor(t); var pInfos = t.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy) .Where(pInfo => pInfo.GetSetMethod() != null) .ToList(); var fInfos = t.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy) //http://stackoverflow.com/questions/40820102/reflection-returns-backing-fields-of-read-only-properties .Where(fInfo => fInfo.GetCustomAttribute<CompilerGeneratedAttribute>() == null) .ToList(); //Get all inherited fields up the hierarchy => BindingFlags.FlattenHierarchy only works with public members bool inheritance = t.BaseType != null; Type inheritedT = t; Type baseT; while (inheritance) { //inherited fields baseT = inheritedT.BaseType; var baseFInfos = baseT.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy) .Where(fInfo => fInfo.GetCustomAttribute<CompilerGeneratedAttribute>() == null) .ToList(); fInfos = fInfos.Union(baseFInfos).ToList(); //inherit mapper configurations MapperStore store = new MapperStore(); if (store.GetMapper(baseT) != null) CopyConfigurations(baseT, t); inheritance = baseT.BaseType != null; inheritedT = baseT; } IEnumerable<MemberInfo> mInfos = pInfos.Union((IEnumerable<MemberInfo>)fInfos); Dictionary<MemberInfo, MemberTypeInfo> preMTInfos = (Dictionary<MemberInfo, MemberTypeInfo>)mInfos .Select(x => { if (_Ignores.ContainsKey(t) && _Ignores[t].Contains(x.Name)) return new KeyValuePair<MemberInfo, MemberTypeInfo>(x, MemberTypeInfo.Ignore); else return new KeyValuePair<MemberInfo, MemberTypeInfo>(x, MemberTypeInfo.BuiltIn); }) .ToDictionary(x => x.Key, x => x.Value); //prevent collection was modified exception Dictionary<MemberInfo, MemberTypeInfo> changes = new Dictionary<MemberInfo, MemberTypeInfo>(preMTInfos); IEnumerable<string> preNamesList = pInfos .Where(pInfo => preMTInfos[pInfo] != MemberTypeInfo.IEnumerable) .Select(pInfo => pInfo.Name) .Union(fInfos .Where(pInfo => preMTInfos[pInfo] != MemberTypeInfo.IEnumerable) .Select(fInfo => fInfo.Name)); //Store all MemberTypeInfo //Trying to save iterations doing first if dictionary.contains(type) if (_MembersCreators.ContainsKey(t) && _NestedProperties.ContainsKey(t)) { //Set members type dictionary foreach (KeyValuePair<MemberInfo, MemberTypeInfo> kvp in preMTInfos.Where(kvp => kvp.Value != MemberTypeInfo.Ignore)) { if (_MembersCreators[t].ContainsKey(kvp.Key.Name)) { if (!_OnlyConstructor.Contains(t)) changes[kvp.Key] = MemberTypeInfo.Creator;//_preMTInfos[kvp.Key] = MemberTypeInfo.Creator; RemoveMember(kvp.Key, pInfos, fInfos); } else { if (_NestedProperties[t].Contains(kvp.Key.Name)) { if (!_OnlyConstructor.Contains(t)) changes[kvp.Key] = MemberTypeInfo.Nested;//_preMTInfos[kvp.Key] = MemberTypeInfo.Nested; RemoveMember(kvp.Key, pInfos, fInfos); } Type mType = GetMemberType(kvp.Key); if (typeof(IEnumerable).IsAssignableFrom(mType) && !typeof(string).IsAssignableFrom(mType)) { if (!_OnlyConstructor.Contains(t)) changes[kvp.Key] = changes[kvp.Key] | MemberTypeInfo.IEnumerable; //_preMTInfos[kvp.Key] = _preMTInfos[kvp.Key] | MemberTypeInfo.IEnumerable; RemoveMember(kvp.Key, pInfos, fInfos); } } } } else if (_MembersCreators.ContainsKey(t)) { //Set members type dictionary foreach (KeyValuePair<MemberInfo, MemberTypeInfo> kvp in preMTInfos.Where(kvp => kvp.Value != MemberTypeInfo.Ignore)) { if (_MembersCreators[t].ContainsKey(kvp.Key.Name)) { if (!_OnlyConstructor.Contains(t)) changes[kvp.Key] = MemberTypeInfo.Creator; //_preMTInfos[kvp.Key] = MemberTypeInfo.Creator; RemoveMember(kvp.Key, pInfos, fInfos); } else { Type mType = GetMemberType(kvp.Key); if (typeof(IEnumerable).IsAssignableFrom(mType) && !typeof(string).IsAssignableFrom(mType)) { if (!_OnlyConstructor.Contains(t)) changes[kvp.Key] = changes[kvp.Key] | MemberTypeInfo.IEnumerable; //_preMTInfos[kvp.Key] = _preMTInfos[kvp.Key] | MemberTypeInfo.IEnumerable; RemoveMember(kvp.Key, pInfos, fInfos); } } } } else if (_NestedProperties.ContainsKey(t)) { //Add to members names list preNamesList = preNamesList.Union(_NestedProperties[t]); //Set members type dictionary foreach (KeyValuePair<MemberInfo, MemberTypeInfo> kvp in preMTInfos.Where(kvp => kvp.Value != MemberTypeInfo.Ignore)) { if (_NestedProperties[t].Contains(kvp.Key.Name)) { if (!_OnlyConstructor.Contains(t)) changes[kvp.Key] = MemberTypeInfo.Nested; //_preMTInfos[kvp.Key] = MemberTypeInfo.Nested; RemoveMember(kvp.Key, pInfos, fInfos); } Type mType = GetMemberType(kvp.Key); if (typeof(IEnumerable).IsAssignableFrom(mType) && !typeof(string).IsAssignableFrom(mType)) { if (!_OnlyConstructor.Contains(t)) changes[kvp.Key] = changes[kvp.Key] | MemberTypeInfo.IEnumerable; //_preMTInfos[kvp.Key] = _preMTInfos[kvp.Key] | MemberTypeInfo.IEnumerable; RemoveMember(kvp.Key, pInfos, fInfos); } } } //built-in else if (!_OnlyConstructor.Contains(t)) { //Set members type dictionary foreach (KeyValuePair<MemberInfo, MemberTypeInfo> kvp in preMTInfos.Where(kvp => kvp.Value != MemberTypeInfo.Ignore)) { Type mType = GetMemberType(kvp.Key); if (typeof(IEnumerable).IsAssignableFrom(mType) && !typeof(string).IsAssignableFrom(mType)) changes[kvp.Key] = MemberTypeInfo.IEnumerable; //_preMTInfos[kvp.Key] = MemberTypeInfo.IEnumerable; } } if (_Interfaces.ContainsKey(t)) { //Set members type dictionary foreach (KeyValuePair<MemberInfo, MemberTypeInfo> kvp in preMTInfos.Where(kvp => kvp.Value != MemberTypeInfo.Ignore)) { if (_Interfaces[t].Contains(kvp.Key.Name)) { if (!_OnlyConstructor.Contains(t)) changes[kvp.Key] = changes[kvp.Key] | MemberTypeInfo.Interface; RemoveMember(kvp.Key, pInfos, fInfos); } Type mType = GetMemberType(kvp.Key); if (typeof(IEnumerable).IsAssignableFrom(mType) && !typeof(string).IsAssignableFrom(mType)) { if (!_OnlyConstructor.Contains(t)) changes[kvp.Key] = changes[kvp.Key] | MemberTypeInfo.IEnumerable; //_preMTInfos[kvp.Key] = _preMTInfos[kvp.Key] | MemberTypeInfo.IEnumerable; RemoveMember(kvp.Key, pInfos, fInfos); } } } //Lock-static dictionaries lock (_LockObject) { if (!_mtInfos.ContainsKey(t)) _mtInfos.Add(t, changes); else _mtInfos[t] = changes; if (!_NamesList.ContainsKey(t)) _NamesList.Add(t, preNamesList); else _NamesList[t] = preNamesList; if (!_QBPropertyInfos.ContainsKey(t)) _QBPropertyInfos.Add(t, pInfos.ToArray()); else _QBPropertyInfos[t] = pInfos.ToArray(); if (!_QBFieldInfos.ContainsKey(t)) _QBFieldInfos.Add(t, fInfos.ToArray()); else _QBFieldInfos[t] = fInfos.ToArray(); string columns = ""; var orderedMembersNames = _QBPropertyInfos[t].Select(x => x.Name) .Union(_QBFieldInfos[t].Select(x => QBuilder.StringSQLBuilder.RemoveFieldsUnderscore(x.Name))) .OrderBy(x => x); columns = string.Join(",", orderedMembersNames); if (!_Columns.ContainsKey(t)) _Columns.Add(t, columns); else _Columns[t] = columns; } }
It iterates through all members of the given type (and parent classes), storing them, determining who are an
IEnumerable
and filling another important dictionary:protected static Dictionary<Type, Dictionary<MemberInfo, MemberTypeInfo>> _mtInfos;
Which is just a fast way to determine at runtime what have to do the mapper.
MemberTypeInfo
is anEnum
:[Flags] public enum MemberTypeInfo { BuiltIn = 1, Nested = 2, Creator = 4, IEnumerable = 8, Dictionary = 16, Interface = 32, Ignore = 64 }
So, at runtime the mapper looks at this dictionary for every member, and does whatever it has to do, e.g. if the member is a nested type it calls another mapper recursively to map the member, and if it's built-in type it just search the value in the given Dapper
dynamic
result by the member's name and sets it.
The configurations of the parent classes are inherited too through theprivate void CopyConfigurations(Type baseT, Type nestedT)
method, so one can configure a parent class completely (even an abstract class), and then configure all the child class without having to repeat the parent's things, since all child will automatically inherit parent configurations.
There're some obvious configurations like AddNestedProperty
or IgnoreProperty
, and others not so obvious like AddInterfaceToObjectCondition
but I think that the methods comments are enough to understand what they all do.
If that's not the case I'll explain anything, just ask it.
Just to note a pair of rules here (or flaws...):
- The method
EndConfig()
must be called for every type that will have to be mapped, even nested types, interfaces, structs, everything. Even if the type have only built in types and needs no other configuration. The mapper only "see" what's configured, ifEndConfig()
doesn't get called for a type, that type doesn't exist for the mapper. - The same method must be called after all configurations for that type have been called. Given that it's fluent type, the last method for a type configuration should be always
EndConfig()
. Take in mind that the methodSetMembersInformation
compares members with what the user stored in dictionaries, so if that method is called various times, I have no idea what could happen, it's not prepared for that. - I wouldn't make configurations at run time. The system haven't been made for that, I haven't tested it and I have no ides what would happen. Maybe it'd work flawlessly, or maybe not.
A pair of example of some configurations of my own project:
//A pair of easy ones
mConfig
.AddNestedProperty<Apunte>(false, "_Asiento")
.AddMemberCreator<Apunte>("_DebeHaber", x => (DebitCredit)x.DebeHaber)
.AddPrefixes<Apunte>(new string[] { "apu" })
.EndConfig<Apunte>();
mConfig
.AddConstructor<ApunteDLO>(x => new ApunteDLO(x.Id, x.IdOwnerComunidad, x.OrdenEnAsiento, x.Asiento, x.Concepto, x.DebeHaber,
x.Importe, x.IdCuenta, x.Punteo, x.Factura))
.MapOnlyConstructor<ApunteDLO>()
.EndConfig<ApunteDLO>();
...
//More complicated
mConfig
.AddConstructor<Finca>(x => new Finca(x.Id, x.IdOwnerComunidad, x.Baja, x.Nombre, x.Coeficiente, x.Codigo, this.ACData))
.AddNestedProperty<Finca>(false, "_PropietarioActual")
.AddDictionary<Finca>("_HistoricoPropietarios", new string[2] { "FechaFinal", "IdPropietario" })
.AddNestedProperty<Finca>(false, "_Cuotas")
.AddDictionary<Finca>("_Cuotas", new string[2] { "cuotaId", "Cuota" })
.AddNestedProperty<Finca>(false, "_EntregasACuenta")
.AddNestedProperty<Finca>(false, "_Devoluciones")
.AddNestedProperty<Finca, DireccionPostal>(false, x => x.Direccion)
.AddMemberCreator<Finca, DireccionPostal>(x => x.Direccion2,
x => new DireccionPostal(x.TipoVia2, x.Direccion2, x.CP2, x.Localidad2, x.Provincia2))
.AddMemberCreator<Finca, TipoPagoCuotas>(x => x.DefaultTipoPagoCuotas, x => (TipoPagoCuotas)x.DefaultTipoPagoCuotas)
.AddMemberCreator<Finca, sTelefono>(x => x.Telefono1, x => new sTelefono(x.Telefono, x.TipoTelefono))
.AddMemberCreator<Finca, sTelefono>(x => x.Telefono2, x => new sTelefono(x.Telefono2, x.TipoTelefono2))
.AddMemberCreator<Finca, sTelefono>(x => x.Telefono3, x => new sTelefono(x.Telefono3, x.TipoTelefono3))
.AddMemberCreator<Finca, sTelefono>(x => x.Fax, x => new sTelefono(x.Fax, TipoTelefono.Fax))
.AddMemberCreator<Finca, int[]>(x=>x.IdAsociadas, x=>
{
IEnumerable<dynamic> ex = (IEnumerable<dynamic>)x;
return ex
.Select(dyn => dyn.IdFincaAsociada)
.Distinct()
.ToArray();
})
.AddMemberCreator<Finca>("_IdCopropietarios", x =>
{
IEnumerable<dynamic> ex = (IEnumerable<dynamic>)x;
return ex
.Select(dyn => dyn.IdPersonaCoProp)
.Distinct()
.ToArray();
})
.AddMemberCreator<Finca>("_IdPagadores", x =>
{
IEnumerable<dynamic> ex = (IEnumerable<dynamic>)x;
return ex
.Select(dyn => new Tuple<int, TipoPagoCuotas>(dyn.IdPersonaCoPag, (TipoPagoCuotas)dyn.TipoPagoCuotas))
.Distinct()
.ToArray();
})
.EndConfig<Finca>();
MAPPERSTORE
I thought it would be a waste to create mappers all around the app, so I just created a little class called MapperStore
to store and retrieve mappers already created, using a static dictionary to store mappers as interfaces.
/// <summary>
/// Use this to get DapperMapper objects.
/// </summary>
public class MapperStore : DMStatic_Store
{
/// <summary>
/// Store t as a type that can be, and has been configured for mapping by DapperMapper. If you are configuring a type, use
/// MapperConfig.EndCongfig<type>() instead.
/// Don't use it before configuring the mapper first.
/// </summary>
/// <param name="t"></param>
public void StoreType(Type t)
{
if (!_TypesToMap.Contains(t))
_TypesToMap.Add(t);
}
/// <summary>
/// Store a mapper object in static dictionary.
/// </summary>
/// <param name="t"></param>
/// <param name="mapper"></param>
public void StoreMapper(Type t, iDapperMapper mapper)
{
if (!_Mappers.ContainsKey(t)) _Mappers.Add(t, mapper);
}
/// <summary>
/// Get mapper of type t. If type t hasn't been stored by StoreType, returns null. If type t has been stored and there are no mappers
/// created yet, it creates a new one, stores it, and return it.
/// </summary>
/// <param name="t"></param>
/// <returns></returns>
public iDapperMapper GetMapper(Type t)
{
if (_Mappers.ContainsKey(t))
return _Mappers[t];
if (!_TypesToMap.Contains(t))
return null;
iDapperMapper mapper = (iDapperMapper)Activator.CreateInstance(typeof(DapperMapper<>).MakeGenericType(t), this);
StoreMapper(t, mapper);
return mapper;
}
/// <summary>
/// Returns true if a mapper exists or can be created, and set it as iDapperMapper.
/// If type t hasn't been stored by StoreType, returns false. If type t has been stored and there are no mappers
/// created yet, it creates a new one, stores it, set it as iDapperMapper and returns true.
/// </summary>
/// <param name="t"></param>
/// <param name="mapper"></param>
/// <returns></returns>
public bool GetMapper(Type t, out iDapperMapper mapper)
{
if (_Mappers.ContainsKey(t))
{
mapper = _Mappers[t];
return true;
}
if (!_TypesToMap.Contains(t))
{
mapper = null;
return false;
}
mapper = (iDapperMapper)Activator.CreateInstance(typeof(DapperMapper<>).MakeGenericType(t), this);
StoreMapper(t, mapper);
return false;
}
/// <summary>
/// Remove mapper previously stored.
/// </summary>
/// <param name="t"></param>
public void RemoveMapper(Type t)
{
if (_Mappers.ContainsKey(t))
_Mappers.Remove(t);
}
}
And the expected way to get a mapper:
MapperStore store = new MapperStore();
DapperMapper<Foo> mapper = (DapperMapper<Foo>)store.GetMapper(typeof(Foo));
DAPPERMAPPER
And here's the thing finally, it's the class that actually map the objects, usually through this method:
public T Map(IEnumerable<dynamic> dapperResult, bool cleanResult = false)
{
var parser = new PrePostFixesParser(this);
T mapped = this.NewObject(dapperResult.First());
if (_OnlyConstructor.Contains(this.TType)) return mapped;
//TODO: divide el siguiente foreach en dos con dos nuevos diccionarios estáticos, uno para pInfos y otro para fInfos,
//aunque se repita código: hacer métodos para cada parte del código del tipo:
//private T PreMapCreator(KeyValuePair<PropertyInfo, MemberTypeInfo> kvp, IEnumerable<dynamic> dapperResult, bool cleanResult = false)
//private T PreMapIEnumerable(KeyValuePair<PropertyInfo, MemberTypeInfo> kvp, IEnumerable<dynamic> dapperResult, bool cleanResult = false)
//...
//Loop through all members
foreach (KeyValuePair<MemberInfo, MemberTypeInfo> kvp in mtInfos)
{
if (kvp.Value == MemberTypeInfo.Ignore)
continue;
//Member have a creator
else if ((kvp.Value & MemberTypeInfo.Creator) == MemberTypeInfo.Creator)
{
//MemberDelegate mDel = (MemberDelegate)_MembersCreators[this.TType][kvp.Key.Name];
Func<dynamic, object> mDel = (Func<dynamic, object>)_MembersCreators[this.TType][kvp.Key.Name];
if (kvp.Key.MemberType == MemberTypes.Property) ((PropertyInfo)kvp.Key).SetValue(mapped, mDel(dapperResult));
else ((FieldInfo)kvp.Key).SetValue(mapped, mDel(dapperResult));
}
//Member is IDictionary or IEnumerable
else if ((kvp.Value & MemberTypeInfo.IEnumerable) == MemberTypeInfo.IEnumerable)
{
Type t = GetMemberType(kvp.Key);
//if ((kvp.Value & MemberTypeInfo.Interface) == MemberTypeInfo.Interface) t = ResolveInterface(kvp.Key, dapperResult);
//else t = GetMemberType(kvp.Key);
/*
{
//Type of property or field
if (kvp.Key.MemberType == MemberTypes.Property) t = ((PropertyInfo)kvp.Key).PropertyType;
else t = ((FieldInfo)kvp.Key).FieldType;
}*/
bool isAnInterface = (kvp.Value & MemberTypeInfo.Interface) == MemberTypeInfo.Interface;
bool isNested = (kvp.Value & MemberTypeInfo.Nested) == MemberTypeInfo.Nested;
//If member is a dictionary
if (typeof(IDictionary).IsAssignableFrom(t))
{
//Create a dummy dictionary with the dapper's dynamic result which should be equal to the final one
DictionaryMapper dictMapper = new DictionaryMapper(dapperResult, kvp.Key.Name, isNested, isAnInterface, cleanResult, t, this);
try
{
if (kvp.Key.MemberType == MemberTypes.Property) ((PropertyInfo)kvp.Key).SetValue(mapped, dictMapper.DummyDictionary);
else ((FieldInfo)kvp.Key).SetValue(mapped, dictMapper.DummyDictionary);
}
catch (Exception err)
{
throw new CustomException_DapperMapper(
$@"DapperMapper.Map: Couldn't map IDictionary member {kvp.Key.Name} with value contained by dynamic object.
Incorrect type of value?: {kvp.Value.ToString()}",
err);
}
}
//Rest of enumerables
else
{
IEnumerable<dynamic> iEnumDapperResult;
//Select current member's values from dynamic
if (isNested && !cleanResult)
{
//Type mType = t; // GetMemberType(kvp.Key);//IEnumerable<T>
Type genericType = t.GenericTypeArguments[0];//mType.GenericTypeArguments[0];//T
if ((kvp.Value & MemberTypeInfo.Interface) == MemberTypeInfo.Interface)
{
bool genericIsInterfaceNotIEnumerable =
genericType.IsInterface &&
!typeof(IDictionary).IsAssignableFrom(genericType) &&
!(typeof(IEnumerable).IsAssignableFrom(genericType) && !typeof(string).IsAssignableFrom(genericType));
if (genericIsInterfaceNotIEnumerable) genericType = ResolveInterface(genericType, dapperResult);
}
iDapperMapper nestedMapper = MappersStore.GetMapper(genericType);
var nestedParser = new PrePostFixesParser(nestedMapper);
iEnumDapperResult = dapperResult
.Select(dyn => nestedParser.GetTypeMembersWithoutPrePostFixes(dyn, nestedMapper.NamesList));
}
else if (!cleanResult) iEnumDapperResult = dapperResult.Select(dyn => parser.RemovePrePostFixesFromDictionary(dyn));
else iEnumDapperResult = dapperResult;
//Create dummy IEnumerable
EnumerableMapper enumMapper = new EnumerableMapper(iEnumDapperResult, kvp.Key.Name, isNested, t, this.TType); ;
var dummy = Activator.CreateInstance(t, enumMapper.DummyEnumerable);
try
{
if (kvp.Key.MemberType == MemberTypes.Property) ((PropertyInfo)kvp.Key).SetValue(mapped, dummy);
else ((FieldInfo)kvp.Key).SetValue(mapped, dummy);
}
catch (Exception err)
{
throw new CustomException_DapperMapper(
$@"DapperMapper.Map: Couldn't map IEnumerable member {kvp.Key.Name} with value contained by dynamic object.
Incorrect type of value?: {kvp.Value.ToString()}",
err);
}
}
}//End IDictionary/IEnumerable
//If Built-in
else if ((kvp.Value & MemberTypeInfo.BuiltIn) == MemberTypeInfo.BuiltIn)
{
string name = parser.RemoveFieldsUnderscore(kvp.Key.Name);
IDictionary<string, object> dapperDict;
if (!cleanResult)
dapperDict = parser.GetTypeMembersWithoutPrePostFixes(dapperResult.First(), NamesList) as IDictionary<string, object>;
else
dapperDict = dapperResult.First() as IDictionary<string, object>;
if (!dapperDict.ContainsKey(name))
throw new CustomException_DapperMapper(
$@"DapperMapper.Map: There's no member in dynamic dapper result with name {kvp.Key.Name}. Cannot Map object.");
try
{
if (kvp.Key.MemberType == MemberTypes.Property) ((PropertyInfo)kvp.Key).SetValue(mapped, dapperDict[name]);
else ((FieldInfo)kvp.Key).SetValue(mapped, dapperDict[name]);
}
catch (Exception err)
{
throw new CustomException_DapperMapper(
$@"DapperMapper.Map: Couldn't map BuiltIn-type member {kvp.Key.Name} with value contained by dynamic object.
Incorrect type of value?: {kvp.Value.ToString()}",
err);
}
}
//if nested
else if ((kvp.Value & MemberTypeInfo.Nested) == MemberTypeInfo.Nested)
{
Type mType = GetMemberType(kvp.Key);
if ((kvp.Value & MemberTypeInfo.Interface) == MemberTypeInfo.Interface)
mType = ResolveInterface(mType, dapperResult);
//access generic Map method through nongeneric interface method
iDapperMapper nestedMapper = MappersStore.GetMapper(mType);
if (nestedMapper == null)
throw new CustomException_DapperMapper(
$@"DapperMapper.Map: No Mapper found at store for property {kvp.Key.Name} of type {mType.ToString()}.
If you want to map a nested property you have to create a mapper for that property type.");
if (kvp.Key.MemberType == MemberTypes.Property)
((PropertyInfo)kvp.Key).SetValue(mapped, nestedMapper.NoGenericMap(dapperResult, cleanResult));
else ((FieldInfo)kvp.Key).SetValue(mapped, nestedMapper.NoGenericMap(dapperResult, cleanResult));
}
}
return mapped;
}
The idea is to use:
MapperStore store = new MapperStore();
DapperMapper<Persons> mapper = (DapperMapper<Persons>)store.GetMapper(typeof(Persons));
Persons p = mapper.Map(dynamicDapperResult);
Due to it being generic and that the method has to recursively call other DapperMapper.Map
methods, I use a non-generic interface to call the mappers while mapping at run time, using the NoGenericMap
methods.
The generic DapperMapper<T>
class actually maps the objects and is called at runtime. It uses the dictionaries to know if a member is nested or built-in, if it's an interface, if it's an IEnumerable
or IDictionary
, if it have to use a Func
to create a member, etc. It actually does very little with reflection, the minimum I could manage to achieve.
Basically it first create an instance of the object (via reflection or with the configured constructor), then loops through all class members, search in the dictionaries for adequate type and member using the MemberTypeInfo
Enum I spoke about above and fill the member according to what was configured following the priority showed in the diagram. If a member is a nested type, just create a new mapper and again and again.
IEnumerable
and IDictionary
members have a their own classes to map them, EnumerableMapper
and DictionaryMapper
. They simply create an instance of the adequate object and search the data to map all the collection members through the dynamic
Dapper's result, which will call again a normal DapperMapper
if they are configured as nested types.
There's an overloaded method to map enumerables:
public R Map<R>(IEnumerable<dynamic> dapperResult, string splitOn = "Id", bool cleanResult = false)
where R : IEnumerable<T>
One must call this method if want to map a whole IEnumerable
of objects:
List<Persons> myList = mapper.Map<List<Persons>>(dapperResult, "Id");
I didn't find other way to do this except with an overloaded method.
In general the Dapper result dynamic
members must have the same name as the object to be mapped members. I wrote the code to support prefixes and sufixes, that can be added via MapperConfig
, and a parser to "decode" them at runtime.
The parser:
public class PrePostFixesParser
{
public PrePostFixesParser(iDapperMapper mapper)
{
Tuple<string[], bool> prefixes = mapper != null ? mapper.Prefixes : null;
Tuple<string[], bool> postfixes = mapper != null ? mapper.Postfixes : null;
if (prefixes != null)
{
this.Prefixes = prefixes.Item1;
this.PrefixesExclusive = prefixes.Item2;
this.PrefixesCount = prefixes.Item1.Select(x => x.Count()).Distinct();
}
else
{
this.Prefixes = null;
this.PrefixesExclusive = false;
this.PrefixesCount = new int[] { 0 };
}
if (postfixes != null)
{
this.Postfixes = postfixes.Item1;
this.PostfixesExclusive = postfixes.Item2;
this.PostfixesCount = postfixes.Item1.Select(x => x.Count()).Distinct();
}
}
#region properties
public string[] Prefixes { get; private set; }
public string[] Postfixes { get; private set; }
public bool PrefixesExclusive { get; private set; }
public bool PostfixesExclusive { get; private set; }
public IEnumerable<int> PrefixesCount { get; private set; }
public IEnumerable<int> PostfixesCount { get; private set; }
#endregion
#region helpers
private bool RemovePrefixIfStringContains(ref string str)
{
if (Prefixes == null)
return false;
bool contain = false;
foreach (int count in PrefixesCount)
{
//contains some of the configurated prefixes
contain = Prefixes.Contains(str.Substring(0, count));
if (contain)
{
str = str.Remove(0, count);
return contain;
}
}
return contain;
}
private bool RemovePostfixIfStringContains(ref string str)
{
if (Postfixes == null)//(PostfixesCount.Count() == 1 && PostfixesCount.First() == 0)
return false;
bool contain = false;
foreach (int count in PostfixesCount)
{
contain = Postfixes.Contains(str.Substring(str.Length - count, count));
if (contain)
{
str = str.Remove(str.Length - count, count);
return contain;
}
}
return contain;
}
private bool KeysContainsSomePrePostfix(IDictionary<string, object> dict)
{
if (Prefixes != null && Postfixes != null)
{
return dict.Any(kvp =>
{
foreach (int count in PrefixesCount)
{
//contains some of the configurated prefixes or postfixes
if (Prefixes.Contains(kvp.Key.Substring(0, count))
|| Postfixes.Contains(kvp.Key.Substring(kvp.Key.Length - count, count)))
return true;
else return false;
}
return false;
});
}
else if (Prefixes != null)
{
return dict.Any(kvp =>
{
foreach (int count in PrefixesCount)
{
//contains some of the configurated prefixes
if (Prefixes.Contains(kvp.Key.Substring(0, count)))
return true;
else return false;
}
return false;
});
}
else if (Postfixes != null)
{
return dict.Any(kvp =>
{
foreach (int count in PrefixesCount)
{
//contains some of the configurated postfixes
if (Postfixes.Contains(kvp.Key.Substring(kvp.Key.Length - count, count)))
return true;
else return false;
}
return false;
});
}
return false;
//return dict.Any(kvp => (Prefixes.Contains(kvp.Key.Substring(0,)
}
#endregion
#region public methods
public string RemoveFieldsUnderscore(string str)
{
if (str.StartsWith("_")) return str.Remove(0, 1);
return str;
}
public IEnumerable<string> GetCleanNamesList(IEnumerable<string> namesList)
{
return namesList.Select(str => RemoveFieldsUnderscore(str));
}
public IEnumerable<IDictionary<string, object>> RemovePrePostFixesFromDictionary(IEnumerable<dynamic> dyn)
{
List<Dictionary<string, object>> ienum = new List<Dictionary<string, object>>();
foreach (dynamic d in dyn)
{
Dictionary<string, object> dict = new Dictionary<string, object>();
foreach (KeyValuePair<string, object> kvp in (d as IDictionary<string, object>))
{
string keyWithoutPrePostfixes = kvp.Key;
if (!RemovePrefixIfStringContains(ref keyWithoutPrePostfixes) && !RemovePostfixIfStringContains(ref keyWithoutPrePostfixes))
dict.Add(kvp.Key, kvp.Value);
else
dict.Add(keyWithoutPrePostfixes, kvp.Value);//return new KeyValuePair<string, object>(keyWithoutPrePostfixes, kvp.Value);
}
ienum.Add(dict);
}
return ienum;
}
public dynamic GetTypeMembersWithoutPrePostFixes(dynamic dyn, IEnumerable<string> namesList)
{
var typeMembers = new ExpandoObject() as IDictionary<string, object>;
IDictionary<string, object> dict = dyn as IDictionary<string, object>;
IEnumerable<string> names = namesList.Select(str => RemoveFieldsUnderscore(str));
int i = 0;
if (!PrefixesExclusive && !PostfixesExclusive)
{
foreach (KeyValuePair<string, object> kvp in dict)
{
string keyWithoutPrePostfixes = kvp.Key;
bool namesContainsKey = names.Contains(kvp.Key);
if (!RemovePrefixIfStringContains(ref keyWithoutPrePostfixes)
&& !RemovePostfixIfStringContains(ref keyWithoutPrePostfixes)
&& namesContainsKey)
{
typeMembers.Add(kvp.Key, kvp.Value);
i++;
}
else if (names.Contains(keyWithoutPrePostfixes))
{
typeMembers.Add(keyWithoutPrePostfixes, kvp.Value);
i++;
}
}
}
else if (PrefixesExclusive && PostfixesExclusive)
{
foreach (KeyValuePair<string, object> kvp in dict)
{
string keyWithoutPrePostfixes = kvp.Key;
if (RemovePrefixIfStringContains(ref keyWithoutPrePostfixes)
&& RemovePostfixIfStringContains(ref keyWithoutPrePostfixes)
&& names.Contains(keyWithoutPrePostfixes))
typeMembers.Add(keyWithoutPrePostfixes, kvp.Value);
}
}
else if (PrefixesExclusive)
{
foreach (KeyValuePair<string, object> kvp in dict)
{
string keyWithoutPrePostfixes = kvp.Key;
if (RemovePrefixIfStringContains(ref keyWithoutPrePostfixes))
{
RemovePostfixIfStringContains(ref keyWithoutPrePostfixes);
if (names.Contains(keyWithoutPrePostfixes))
typeMembers.Add(keyWithoutPrePostfixes, kvp.Value);
}
}
}
else
{
foreach (KeyValuePair<string, object> kvp in dict)
{
string keyWithoutPrePostfixes = kvp.Key;
if (RemovePostfixIfStringContains(ref keyWithoutPrePostfixes))
{
RemovePrefixIfStringContains(ref keyWithoutPrePostfixes);
if (names.Contains(keyWithoutPrePostfixes))
typeMembers.Add(keyWithoutPrePostfixes, kvp.Value);
}
}
}
return typeMembers;
}
#endregion
}
And that's all, I think. If you are still reading this I believe I have to thank you and congratulate you :P
Besides that, I'll just post the test I did that I mentioned at the beginning (and the results with a simple object and with an object with dictionary members), and that will be all:
public static void MapperTest<T>(int testsTotal, int desde, int cada, int idAlias = 0, bool deleteFirst = true)
{
Type t = typeof(T);
string filepath = @"E:\TESTMAPPER." + t.Name + ".xlsx";
if (deleteFirst && File.Exists(filepath))
File.Delete(filepath);
var file = new FileInfo(filepath);
var package = new ExcelPackage(file);
ExcelWorksheet wSheet = package.Workbook.Worksheets.Add("prueba1");
wSheet.Cells[1, 1].Value = $"MAPPERTEST: MAP {t.Name} ; Tests: { testsTotal.ToString()}. Desde: { desde.ToString()}. Cada: { cada.ToString()}.";
wSheet.Cells[2, 1].Value = "ID";
wSheet.Cells[2, 2].Value = "TYPE";
wSheet.Cells[2, 3].Value = "OBJECTS CREATED";
wSheet.Cells[2, 4].Value = "SECONDS";
wSheet.Cells[2, 5].Value = "MILISECONDS";
wSheet.Cells[2, 6].Value = "SECONDS/OBJECTS";
Console.WriteLine("MAPPERTEST:");
Console.WriteLine($@" 1.- MAP {t.Name}
tests: {testsTotal.ToString()}.
desde: {desde.ToString()}.
cada: {cada.ToString()}.");
Console.WriteLine();
Console.WriteLine(" -WAITING-");
Console.ReadLine();
Console.WriteLine("------------------");
Console.WriteLine();
for (int j = desde, x = 0; j < (testsTotal * cada); j += cada, x++)
{
Console.WriteLine("...");
Console.WriteLine($" Test Nº {x.ToString()}.");
Console.WriteLine($" Nº objetos a crear: {j.ToString()}");
Console.WriteLine();
Console.WriteLine("...");
using (SqlConnection con = new SqlConnection(conString))//(MySqlConnection con = new MySqlConnection(conString))
{
con.Open();
var result = con.Query(@"SELECT p.Id,p.Nombre,pif.pruebaI,pif.otro,a.Id aId,a.Acc aAcc,a.Persona aPersona
FROM Persons p
RIGHT JOIN Acc a ON p.Id=a.Persona
RIGHT JOIN pruebaif pif ON p.Id=pif.Persona
WHERE p.Id=1");
Stopwatch watch = new Stopwatch();
watch.Reset();
watch.Start();
for (int i = 0; i < j; i++)
{
T p;
MapperStore store = new MapperStore();
DapperMapper<T> mapper = (DapperMapper<T>)store.GetMapper(t);
p = mapper.Map(result);
}
watch.Stop();
Console.WriteLine($" +++++++++TEST {t.Name} - {x.ToString()} DONE+++++++++");
Console.WriteLine();
Console.WriteLine($" TIME WATCH: {watch.Elapsed.ToString()}");
Console.WriteLine($" TIME WATCH SECONDS: {watch.Elapsed.Seconds.ToString()}");
Console.WriteLine($" TIME WATCH MILISECONDS: {watch.ElapsedMilliseconds.ToString()}");
Console.WriteLine();
wSheet.Cells[x + 2, 1].Value = x + idAlias;
wSheet.Cells[x + 2, 2].Value = t.Name;
wSheet.Cells[x + 2, 3].Value = j;
wSheet.Cells[x + 2, 4].Value = watch.Elapsed.Seconds;
wSheet.Cells[x + 2, 5].Value = watch.ElapsedMilliseconds;
wSheet.Cells[x + 2, 6].Formula = "D3/C3";
con.Close();
}
Console.WriteLine("------------------");
Console.WriteLine("DB OK");
Console.WriteLine();
}
package.Save();
Console.WriteLine(" +++++++++ALL TESTS DONE+++++++++");
Console.WriteLine();
Console.ReadLine();
}
PS.: There's a SQL query builder in the same Github link that I made for the same project too, but that's another story, it's just string concatenations using some of the mapper dictionaries, just ignore it.
Edit: Asked by @t3chb0t in the comments I'm putting an example of an object to be mapped:
public class Apunte : IObjModelBase, IOwnerComunidad, IObjWithDLO<ApunteDLO>
{
public Apunte() { }
public Apunte(int id, int idComunidad, Asiento asiento, string FacturaId = null)
{
this._Id = id;
this._IdOwnerComunidad = idComunidad;
this._Asiento = asiento;
this._Factura = FacturaId;
}
#region fields
private int _Id;
private int _IdOwnerComunidad;
#pragma warning disable CS0649
private int _OrdenEnAsiento;
#pragma warning restore CS0649
private Asiento _Asiento;
private DebitCredit _DebeHaber;
private decimal _Importe;
private string _Factura;
#endregion
#region properties
public int Id { get { return this._Id; } }
public int IdOwnerComunidad { get { return this._IdOwnerComunidad; } }
public int OrdenEnAsiento { get { return this._OrdenEnAsiento; } }
public Asiento Asiento { get { return this._Asiento; } }
public string Concepto { get; set; }
public DebitCredit DebeHaber
{
get { return this._DebeHaber; }
set
{
if (this._DebeHaber != value)
{
if(!_Asiento.Abierto)
throw new CustomException_ObjModels(
$"Error cambiando DebeHaber de apunte numero {Id} de asiento numero {Asiento.Id}. Asiento cerrado.");
this._Asiento.CambiaSaldo(this, value);
this._DebeHaber = value;
}
}
}
public decimal Importe
{
get { return this._Importe; }
set
{
if (this._Importe != value)
{
if (!_Asiento.Abierto)
throw new CustomException_ObjModels(
$"Error cambiando DebeHaber de apunte numero {Id} de asiento numero {Asiento.Id}. Asiento cerrado.");
this._Asiento.CambiaSaldo(this, value);
this._Importe = value;
}
}
}
public CuentaMayor Cuenta { get; set; }
public bool Punteo { get; set; }
public string Factura { get { return this._Factura; } }
#endregion
#region DLO
public ApunteDLO GetDLO()
{
return new ApunteDLO(Id, IdOwnerComunidad, OrdenEnAsiento, Asiento.Codigo.CurrentCodigo, Concepto, DebeHaber, Importe, Cuenta.NumCuenta.ToString(),
Punteo, Factura);
}
#endregion
}
It's a model I use in the same project I did the mapper for. The database have all the plain data, at the beginning of the APP I configure the type (if you note it, it's the same object I used as an example for the configuration), and the repository map the objects through this mapper when needed, again as I described before in the question.