How can I make AutoMapper to map missing unmapped properties to a dictionary inside the destination object? (Like ExtensionData during serialization)
Example:
class Source
{
public int A {get;set;}
public int B {get;set;}
public int C {get;set;}
}
class Destination
{
public int A {get;set;}
public Dictionary<string, object> D {get;set;}
}
Source s = new Source { A = 1, B = 2, C = 3 };
Destination d = ... // Mapping code
Now I want the following result:
d.A ==> 1
d.D ==> {{ "B", 2 }, { "C", 3 }}
* EDIT *
In the end I am looking for a solution w/o reflection. Meaning: During setup/configuration/initialization reflection is allowed, but during the mapping itself, I do not want any delays caused by reflection.
* EDIT *
I am looking for a generic solution, just like the serializers.
There are a lot of possible solutions for your problem.
I've create a custom value resolver for your property and it works perfectly:
public class CustomResolver : IValueResolver<Source, Destination, Dictionary<string, object>>
{
public Dictionary<string, object> Resolve(Source source, Destination destination, Dictionary<string, object> destMember, ResolutionContext context)
{
destMember = new Dictionary<string, object>();
var flags = BindingFlags.Public | BindingFlags.Instance;
var sourceProperties = typeof(Source).GetProperties(flags);
foreach (var property in sourceProperties)
{
if (typeof(Destination).GetProperty(property.Name, flags) == null)
{
destMember.Add(property.Name, property.GetValue(source));
}
}
return destMember;
}
}
How to use it?
static void Main(string[] args)
{
Mapper.Initialize(cfg => {
cfg.CreateMap<Source, Destination>()
.ForMember(dest => dest.D, opt => opt.ResolveUsing<CustomResolver>());
});
var source = new Source { A = 1, B = 2, C = 3 };
var result = Mapper.Map<Source, Destination>(source);
}
public class Source
{
public int A { get; set; }
public int B { get; set; }
public int C { get; set; }
}
public class Destination
{
public int A { get; set; }
public Dictionary<string, object> D { get; set; }
}
I like Pawel's solution because is more generic.
If you want something simpler but less generic you could initialize the mapper like this:
Mapper.Initialize(cfg => {
cfg.CreateMap<Source, Destination>()
.ForMember(dest => dest.D,
opt => opt.MapFrom(r => new Dictionary<string,object>(){{ "B", r.B},{ "C", r.C}}));
});
You could combine Pawel's answer and Reflection.Emit to make it fast. Be aware that Reflection.Emit is not supported on all platforms (like iOS).
Unlike ExtensionData, this includes all property values from the source. I don't have an elegant solution to determine which properties were already mapped so I just provided an easy way to exclude certain properties.
public class PropertyDictionaryResolver<TSource> : IValueResolver<TSource, object, Dictionary<string, object>>
{
private static readonly PropertyInfo[] Properties = typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance);
private static readonly ConcurrentDictionary<PropertyInfo, Func<TSource, object>> GetterCache = new ConcurrentDictionary<PropertyInfo, Func<TSource, object>>();
public HashSet<MemberInfo> ExcludedProperties;
public PropertyDictionaryResolver()
{
ExcludedProperties = new HashSet<MemberInfo>();
}
public PropertyDictionaryResolver(Expression<Func<TSource, object>> excludeMembers)
{
var members = ExtractMembers(excludeMembers);
ExcludedProperties = new HashSet<MemberInfo>(members);
}
public Dictionary<string, object> Resolve(TSource source, object destination, Dictionary<string, object> existing, ResolutionContext context)
{
var destMember = new Dictionary<string, object>();
foreach (var property in Properties)
{
if (ExcludedProperties.Contains(property)) continue;
var exp = GetOrCreateExpression(property);
var value = exp(source);
if (value != null)
{
destMember.Add(property.Name, value);
}
}
return destMember;
}
/// <summary>
/// Creates and compiles a getter function for a property
/// </summary>
/// <param name="propInfo"></param>
/// <returns></returns>
public Func<TSource, object> GetOrCreateExpression(PropertyInfo propInfo)
{
if (GetterCache.TryGetValue(propInfo, out var existing))
{
return existing;
}
var parameter = Expression.Parameter(typeof(TSource));
var property = Expression.Property(parameter, propInfo);
var conversion = Expression.Convert(property, typeof(object));
var lambda = Expression.Lambda<Func<TSource, object>>(conversion, parameter);
existing = lambda.Compile();
GetterCache.TryAdd(propInfo, existing);
return existing;
}
/// <summary>
/// Pull the used MemberInfo out of a simple expression. Supports the following expression types only:
/// s => s.Prop1
/// s => new { s.Prop1, s.Prop2 }
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="expression"></param>
/// <returns></returns>
public static IEnumerable<MemberInfo> ExtractMembers<T>(Expression<Func<T, object>> expression)
{
if (expression == null)
{
throw new ArgumentNullException(nameof(expression));
}
switch (expression.Body)
{
case MemberExpression memberExpression:
yield return memberExpression.Member;
yield break;
// s => s.BarFromBaseType
case UnaryExpression convertExpression:
if (convertExpression.Operand is MemberExpression exp)
{
yield return exp.Member;
}
yield break;
// s => new { s.Foo, s.Bar }
case NewExpression newExpression:
if (newExpression.Arguments.Count == 0)
{
yield break;
}
foreach (var argument in newExpression.Arguments.OfType<MemberExpression>())
{
yield return argument.Member;
}
yield break;
}
throw new NotImplementedException("Unrecognized lambda expression.");
}
}
And use it like one of these
[TestClass]
public class Examples
{
[TestMethod]
public void AllProperties()
{
var mapper = new Mapper(new MapperConfiguration(p =>
{
p.CreateMap<Source, Destination>()
.ForMember(x => x.A, cfg => cfg.MapFrom(x => x.A))
.ForMember(x => x.D, cfg => cfg.MapFrom<PropertyDictionaryResolver<Source>>());
}));
var source = new Source { A = 1, B = 2, C = 3 };
var d = mapper.Map<Destination>(source);
// {"A":1,"D":{"A":1,"B":2,"C":3}}
}
[TestMethod]
public void ExcludeSingleProperty()
{
var mapper = new Mapper(new MapperConfiguration(p =>
{
p.CreateMap<Source, Destination>()
.ForMember(x => x.A, cfg => cfg.MapFrom(x => x.A))
.ForMember(x => x.D, cfg => cfg.MapFrom(new PropertyDictionaryResolver<Source>(x => x.A)));
}));
var source = new Source { A = 1, B = 2, C = 3 };
var d = mapper.Map<Destination>(source);
// {"A":1,"D":{"B":2,"C":3}}
}
[TestMethod]
public void ExcludeMultipleProperties()
{
var mapper = new Mapper(new MapperConfiguration(p =>
{
p.CreateMap<Source, Destination>()
.ForMember(x => x.A, cfg => cfg.MapFrom(x => x.A))
.ForMember(x => x.D, cfg => cfg.MapFrom(new PropertyDictionaryResolver<Source>(x => new
{
x.A,
x.B
})));
}));
var source = new Source { A = 1, B = 2, C = 3 };
var d = mapper.Map<Destination>(source);
// {"A":1,"D":{"C":3}}
}
}
Related
i am trying to use projection map to map the property values from source to destination as mentioned here in this question Projection mapping looks like as below but getting error near select statement
Error is : The type arguments for the method
'Enumerable.Select<TSource, Tresult>(IEnumerable,
Func<Tsource, int, Tresult>)' cannot be inferred from usage.Try
specifying type arguments explicitly
and below is the code sample
public static IQueryable<TDest> ProjectionMap<TSource, TDest>(IQueryable<TSource> sourceModel)
where TDest : new()
{
var sourceProperties = typeof(TSource).GetProperties().Where(p => p.CanRead);
var destProperties = typeof(TDest).GetProperties().Where(p => p.CanWrite);
var propertyMap = from d in destProperties
join s in sourceProperties on new { d.Name, d.PropertyType } equals new { s.Name, s.PropertyType }
select new { Source = s, Dest = d };
var itemParam = Expression.Parameter(typeof(TSource), "item");
var memberBindings = propertyMap.Select(p => (MemberBinding)Expression.Bind(p.Dest, Expression.Property(itemParam, p.Source)));
var newExpression = Expression.New(typeof(TDest));
var memberInitExpression = Expression.MemberInit(newExpression, memberBindings);
var projection = Expression.Lambda<Func<TSource, TDest>>(memberInitExpression, itemParam);
return sourceModel.Select(projection);
}
and then i am using above method below
private static MechanicalData TransformMechanicalData(MechanicalData sourceMechanicalData, Dictionary<string, MasterSection> masterSectionMappedLibrary)
{
return new MechanicalData()
{
Acoustic = sourceMechanicalData.Acoustic
.Where(a => a != null)
.Select(ProjectionMap<LibraryAcoustic, LibraryAcoustic>(sourceMechanicalData.Acoustic.AsQueryable())).ToList() ?? new(),
}
}
Could any one please let me know where I am doing wrong, many thanks in advance.
Update:
Acoustic = sourceMechanicalData.Acoustic
.Where(a => a != null)
.Select(acoustic => new LibraryAcoustic
{
Id = acoustic.Id,
IsApproved = true,
NoiseCriteria = acoustic.NoiseCriteria,
SourceOfData = acoustic.SourceOfData,
SourceOfDataId = acoustic.SourceOfData.Id,
MasterSection = masterSectionMappedLibrary["Library Acoustic"]
}).ToList() ?? new(),
calling that transformMechanicalData method in below
if (spaceTypeReader.HasRows)
{
while (spaceTypeReader.Read())
{
var id = spaceTypeReader.IsDBNull(0) ? default : Guid.Parse(spaceTypeReader.GetString(0));
var mechanicalDataJson = spaceTypeReader.IsDBNull(1) ? "null" : spaceTypeReader.GetString(1);
var srcMechanicalDataJson = JsonConvert.DeserializeObject<MechanicalData>(mechanicalDataJson);
fixedSpaceTypesMechanicalData[id] = TransformMechanicalData(srcMechanicalDataJson, masterSectionMappedLibrary);
}
}
and mechanical data class be like
public class MechanicalData
{
public List<LibraryAcoustic> Acoustic { get; set; }
.........
}
Update 2:
model for libraryAcoustic
public class LibraryAcoustic
{
public double? NoiseCriteria { get; set; }
[ForeignKey("SourceOfData")]
public Guid? SourceOfDataId { get; set; }
public virtual CodeStandardGuideline SourceOfData { get; set; }
public Guid Id { get; set; }
public MasterSection MasterSection { get; set; }
public bool? IsApproved { get; set; }
}
FROM Model
"Acoustic": [
{
"Id": "d9254132-d11d-48dd-9b74-c0b60d1e4b8a",
"IsApproved": null,
"SourceOfData": {
"Id": "c5bf3585-50b1-4894-8fad-0ac884343935",
"CodeStandardGuidelineType": "GUIDELINE_OR_STANDARD"
},
"MasterSection": null,
"SourceOfDataId": null,
"NoiseCriteria": 1,
}
],
TO model:
"Acoustic": [
{
"Id": "d9254132-d11d-48dd-9b74-c0b60d1e4b8a",
"IsApproved": true,
"SourceOfData": {
"Id": "c5bf3585-50b1-4894-8fad-0ac88434393",
"CodeStandardGuidelineType": "GUIDELINE_OR_STANDARD"
},
"MasterSection": {Name:"test"},
"SourceOfDataId": "c5bf3585-50b1-4894-8fad-0ac884343935",
"NoiseCriteria": 1,
}
],
Test Class update:
SourceOfData sourceOfData = new SourceOfData()
{
Id = new Guid("c5bf3585-50b1-4894-8fad-0ac884343935"),
Name = "test"
};
TestClassA170 testClassA170 = new TestClassA170()
{
Category = "test",
SourceOfData = sourceOfData,
SourceOfDataId = null,
IsApproved = true,
MinOutdoorAirACH = 1,
MinTotalAirACH = 2,
DirectExhaust = DirectExhaust.NO,
PressureRelationship = PressureRelationship.NEGATIVE,
RecirculatedAir = RecirculatedAir.NO,
SpaceFunction = "10"
};
List<TestClassA170> list = new List<TestClassA170>();
list.Add(testClassA170);
You have to create mapping helper class which accespts additionally Dictionary as paraneter:
public static class PropertyMapper<TSource, TDest>
{
private static Expression<Func<TSource Dictionary<string, MasterSection>, TDest>> _mappingExpression;
private static Func<TSource, Dictionary<string, MasterSection>, TDest> _mapper;
static PropertyMapper()
{
_mappingExpression = ProjectionMap();
_mapper = _mappingExpression.Compile();
}
public static Func<TSource, Dictionary<string, MasterSection>, TDest> Mapper => _mapper;
public static string MasterKeyFromClassName(string className)
{
// you have to do that yourself
throw new NotImplementedException();
}
public static Expression<Func<TSource, Dictionary<string, MasterSection>, TDest>> ProjectionMap()
{
var sourceProperties = typeof(TSource).GetProperties().Where(p => p.CanRead);
var destProperties = typeof(TDest).GetProperties().Where(p => p.CanWrite);
var propertyMap =
from d in destProperties
join s in sourceProperties on new { d.Name, d.PropertyType } equals new { s.Name, s.PropertyType }
where d.Name != "MasterSection"
select new { Source = s, Dest = d };
var itemParam = Expression.Parameter(typeof(TSource), "item");
var dictParam = Expression.Parameter(typeof(Dictionary<string, MasterSection>), "dict");
var memberBindings = propertyMap.Select(p => (MemberBinding)Expression.Bind(p.Dest, Expression.Property(itemParam, p.Source))).ToList();
var masterSectionProp = destProperties.FirstOrDefault(s => s.Name == "MasterSection");
if (masterSectionProp != null)
{
Expression<Func<Dictionary<string, MasterSection>, string, MasterSection>> dictRetrievalTemplate = (dict, value) => dict[value];
var masterPropertyBind = Expression.Bind(masterSectionProp, ExpressionReplacer.GetBody(dictRetrievalTemplate, dictParam, Expression.Constant(MasterKeyFromClassName(typeof(TSource).Name)));
memberBindings.Add(masterPropertyBind);
}
var sourceOfDataProp = destProperties.FirstOrDefault(s => s.Name == "SourceOfDataId");
if (sourceOfDataProp != null)
{
memberBindings.Add(Expression.Bind(sourceOfDataProp, Expression.Property(Expression.Property(itemParam, "SourceOfData"), "Id")));
}
var isApprovedProp = destProperties.FirstOrDefault(s => s.Name == "IsApproved");
if (isApprovedProp != null)
{
memberBindings.Add(Expression.Bind(isApprovedProp, Expression.Constant(true)));
}
var newExpression = Expression.New(typeof(TDest));
var memberInitExpression = Expression.MemberInit(newExpression, memberBindings);
var projection = Expression.Lambda<Func<TSource, Dictionary<string, MasterSection>, TDest>>(memberInitExpression, itemParam, dictParam);
return projection;
}
class ExpressionReplacer : ExpressionVisitor
{
readonly IDictionary<Expression, Expression> _replaceMap;
public ExpressionReplacer(IDictionary<Expression, Expression> replaceMap)
{
_replaceMap = replaceMap ?? throw new ArgumentNullException(nameof(replaceMap));
}
public override Expression Visit(Expression exp)
{
if (exp != null && _replaceMap.TryGetValue(exp, out var replacement))
return replacement;
return base.Visit(exp);
}
public static Expression Replace(Expression expr, Expression toReplace, Expression toExpr)
{
return new ExpressionReplacer(new Dictionary<Expression, Expression> { { toReplace, toExpr } }).Visit(expr);
}
public static Expression Replace(Expression expr, IDictionary<Expression, Expression> replaceMap)
{
return new ExpressionReplacer(replaceMap).Visit(expr);
}
public static Expression GetBody(LambdaExpression lambda, params Expression[] toReplace)
{
if (lambda.Parameters.Count != toReplace.Length)
throw new InvalidOperationException();
return new ExpressionReplacer(Enumerable.Range(0, lambda.Parameters.Count)
.ToDictionary(i => (Expression) lambda.Parameters[i], i => toReplace[i])).Visit(lambda.Body);
}
}
}
Then rewrite your function:
private static Expression<MechanicalData TransformMechanicalData(MechanicalData sourceMechanicalData, Dictionary<string, MasterSection> masterSectionMappedLibrary)
{
return new MechanicalData()
{
Acoustic = sourceMechanicalData.Acoustic
.Where(a => a != null)
.Select(a => PropertyMapper<LibraryAcoustic, LibraryAcoustic>.Mapper(a, masterSectionMappedLibrary)).ToList(),
}
}
I am trying to genericize some repeated stuff but struggling with this. I don't even know if it's even possible. I put what I need inside the code block but I'd be happy to clarify if need be. Any help is appreciated.
public static async Task<object> LoadData<T>(IQueryable<T> query, string RequestGroup) where T : class
{
//this works
//var Groupped = query.AsNoTracking().ToList().GroupBy(x => x.GetPropertyValue(RequestGroup)).Select(x => new { key = x.Key, count = x.Distinct().Count() });
//How can I achieve this?
var Groupped = query.GroupBy(x => x.GetPropertyValue(RequestGroup)).Select(x => new { key = x.Key, count = x.Distinct().Count() }).AsNoTracking().ToListAsync();
return new { Data = Groupped };
}
public async Task<JsonResult> GetData()
{
var query = _context.Samples.AsQueryable();
return Json(await LoadData(query, "Description"));
}
public class Sample
{
public int SampleID { get; set; }
public string Description { get; set; }
}
#region Extensions
public static object GetPropertyValue(this object obj, string name)
{
foreach (string part in name.Split('.'))
{
if (obj == null) { return null; }
Type type = obj.GetType();
PropertyInfo info = type.GetProperty(part);
if (info == null) { return null; }
obj = info.GetValue(obj, null);
}
return obj;
}
public static T GetPropertyValue<T>(this object obj, string name)
{
object retval = GetPropertyValue(obj, name);
if (retval == null) { return default(T); }
// throws InvalidCastException if types are incompatible
return (T)retval;
}
#endregion
Thanks coder_b, I used another response from the link you provided and got what I am looking for. Here is the answer if anyone else needs it:
public static async Task<object> LoadData<T>(IQueryable<T> query, string RequestGroup) where T : class
{
//this works
//var Groupped = query.AsNoTracking().ToList().GroupBy(x => x.GetPropertyValue(RequestGroup)).Select(x => new { key = x.Key, count = x.Distinct().Count() });
//How can I achieve this?
//var Groupped = query.GroupBy(x => x.GetPropertyValue(RequestGroup)).Select(x => new { key = x.Key, count = x.Distinct().Count() }).AsNoTracking().ToListAsync();
//Answer
var Groupped = query.GroupBy(GetGroupKey<T>(RequestGroup)).Select(x => new { key = x.Key, count = x.Distinct().Count() }).ToList();
return new { Data = Groupped };
}
public async Task<JsonResult> GetData()
{
var query = _context.Samples.AsQueryable();
return Json(await LoadData(query, "Description"));
}
public class Sample
{
public int SampleID { get; set; }
public string Description { get; set; }
}
#region Extensions
private static Expression<Func<T, string>> GetGroupKey<T>(string property)
{
var parameter = Expression.Parameter(typeof(T));
var body = Expression.Property(parameter, property);
return Expression.Lambda<Func<T, string>>(body, parameter);
}
#endregion
I am trying to do some conditional mapping, and all the documentation and questions I have read through doesn't seem to cover this particular conditional. I was hoping someone here would have the experience or know how on the best approach to this.
I am mapping an object that has two properties. However, I do not want to map EITHER property, if a specific property is of a value. To visualize this:
foreach(var object in objectB) {
If (object.propertyA == "SomeValue")
continue;
else
Mapper.Map<ObjectA>(object);
}
however, I want the AutoMapper equivalent of the above statement. So something more like:
cfg.CreateMap<ObjectB, ObjectA>()
.ForMember(dest => dest.PropertyA, m => m.Condition(source => source.PropertyA != "SomeValue"))
.ForMember(dest => dest.PropertyB, m => m.Condition(source => source.PropertyA != "SomeVAlue" ? source.PropertyB : ignore))
But the above version obviously does not work.
Thank you in advance for your assistance.
Could be achieved using conditional mapping, see Documentation(http://docs.automapper.org/en/latest/Conditional-mapping.html) for details. To cover arrays filtering case I created a custom type converter which is a little bit tricky (see http://docs.automapper.org/en/stable/Custom-type-converters.html). The updated example is below
using AutoMapper;
using System;
using System.Collections.Generic;
namespace ConsoleAppTest2
{
class Program
{
static void Main(string[] args)
{
Mapper.Initialize(cfg => {
//Using specific type converter for specific arrays
cfg.CreateMap<Foo[], FooDto[]>().ConvertUsing(new ArrayFilterTypeConverter<Foo[], FooDto[], Foo, FooDto>(
(src, dest) => (src.Age > 0)
));
cfg.CreateMap<Foo, FooDto>()
.ForMember(dest => dest.Age, opt => opt.Condition(src => (src.Age >= 0)))
.ForMember(dest => dest.CurrentAddress, opt =>
{
opt.Condition(src => (src.Age >= 0));
opt.MapFrom(src => src.Address);
});
});
var foo = new Foo() { Name = "Name", Address = "Address", Age = -1 };
var fooDTO = new FooDto();
var fooArray = new Foo[] {
new Foo() { Name = "Name1", Address = "Address1", Age = -1 },
new Foo() { Name = "Name2", Address = "Address2", Age = 1 },
new Foo() { Name = "Name3", Address = "Address3", Age = 1 }
};
var fooDTOArray = Mapper.Map<Foo[], FooDto[]>(fooArray); //get 2 elements instead of 3
Mapper.Map(foo, fooDTO);
//The result is we skipped Address and Age properties becase Age is negative
Console.ReadLine();
}
}
public class ArrayFilterTypeConverter<TSourceArray, TDestArray, TSource, TDest> : ITypeConverter<TSourceArray, TDestArray>
{
private Func<TSource, TDest, bool> filter;
public ArrayFilterTypeConverter(Func<TSource, TDest, bool> filter)
{
this.filter = filter;
}
public TDestArray Convert(TSourceArray source, TDestArray destination, ResolutionContext context)
{
var sourceArray = source as TSource[];
List<TDest> destList = new List<TDest>();
var typeMap = context.ConfigurationProvider.ResolveTypeMap(typeof(TSource), typeof(TDest));
foreach (var src in sourceArray)
{
var dest = context.Mapper.Map<TSource, TDest>(src);
if (filter(src, dest))
destList.Add(dest);
}
// Little hack to cast array to TDestArray
var result = (TDestArray)(object)destList.ToArray();
return result;
}
}
internal class FooDto
{
public string Name { get; set; }
public string CurrentAddress { get; set; }
public int Age { get; set; }
}
internal class Foo
{
public string Name { get; set; }
public string Address { get; set; }
public int Age { get; set; }
}
}
I have the following code.
public class SyncProperty : Attribute
{
public readonly string PropertyName;
public SyncProperty(string propertyName)
{
this.PropertyName = propertyName;
}
}
public class SyncContact
{
[SyncProperty("first_name")]
public string FirstName { get; set; }
[SyncProperty("last_name")]
public string LastName { get; set; }
[SyncProperty("phone")]
public string Phone { get; set; }
[SyncProperty("email")]
public string Email { get; set; }
}
I need to create an instance of my SyncContact such as
var contact = new SyncContact { FirstName="Test", LastName="Person", Phone="123-123-1234", Email="test#test.com"};
And then with that object I need to create a NameValueCollection where the object's property uses the SyncProperty's PropertyName as the Key in the collection. Then use that to make a post request to an API. So in this case I would end up with a collection like...
collection["first_name"] = "Test"
collection["last_name"] = "Person"
collection["phone"] = "123-123-1234"
collection["email"] = "test#test.com"
How can I do that?
If you want to get some type metadata, you should use Reflection. To read attribute you can use GetCustomAttribute<AttributeType>() extension for MemberInfo.
This extension method builds sync dictionary from type properties decorated with SyncPropertyAttribute (I suggest to use the dictionary instead of NameValueCollection):
public static Dictionary<string, string> ToSyncDictionary<T>(this T value)
{
var syncProperties = from p in typeof(T).GetProperties()
let name = p.GetCustomAttribute<SyncProperty>()?.PropertyName
where name != null
select new {
Name = name,
Value = p.GetValue(value)?.ToString()
};
return syncProperties.ToDictionary(p => p.Name, p => p.Value);
}
Usage:
var collection = contact.ToSyncDictionary();
Output:
{
"first_name": "Test",
"last_name": "Person",
"phone": "123-123-1234",
"email": "test#test.com"
}
Note: if you are going to use contact data in POST request, then you should consider using simple JSON serialization attributes instead of creating your own attributes. E.g. with Json.NET:
public class SyncContact
{
[JsonProperty("first_name")]
public string FirstName { get; set; }
[JsonProperty("last_name")]
public string LastName { get; set; }
[JsonProperty("phone")]
public string Phone { get; set; }
[JsonProperty("email")]
public string Email { get; set; }
}
Then simple serialization will do the job:
string json = JsonConvert.SerializeObject(contact);
And the result will be exactly same as above.
The attributes belong to the properties of the class, so you need to get the type of the class, then find the appropriate properties and then get the custom attributes.
Something like:
var contact = new SyncContact { FirstName="Test", LastName="Person", Phone="123-123-1234", Email="test#test.com"};
var t = typeof(SyncContact);
var props = t.GetProperties().Select(p => new { p, attr = p.GetCustomAttribute<SyncProperty>() }).Where(p => p.attr != null);
var dict = props.ToDictionary(p => p.attr.PropertyName, v => v.p.GetValue(contact) );
This leaves out any properties that aren't tagged, but you can decide to handle those differently if you wanted to.
Fiddle
Assuming SyncProperty is tagged on every property, this should do the job:
var contact = new SyncContact { FirstName="Test", LastName="Person", Phone="123-123-1234", Email="test#test.com"};
var collection = contact.GetType().GetProperties()
.Select(x => new
{
x.GetCustomAttribute<SyncProperty>().PropertyName,
Value = x.GetValue(contact).ToString()
})
.ToDictionary(x => x.PropertyName, x => x.Value);
As a helper method:
public static class SynxHelper
{
public static Dictionary<string, string> Serialize<T>(T obj)
{
return typeof(T).GetProperties()
.Select(x => new
{
SyncProperty = x.GetCustomAttribute<SyncProperty>(),
Value = x.GetValue(obj)
})
.Where(x => x.SyncProperty != null)
.ToDictionary(x => x.SyncProperty.PropertyName, x => x.Value.ToString());
}
}
// usage
var collection = SynxHelper.Serialize(contact);
Here is a one line linq solution for this
(from prop in obj.GetType().GetProperties()
where prop.GetCustomAttribute<SyncProperty>() != null
select new { Key = prop.GetCustomAttribute<SyncProperty>().PropertyName, Value = prop.GetValue(obj) })
.ToDictionary(k => k.Key, v => v.Value);
BUT!!!!!!!! Don't try doing this your self. This is not optimized and slow as all reflection is.
This is just to demonstrate how bad reflection is
static void Main(string[] args)
{
var data = Enumerable.Range(0, 10000).Select(i => new SyncContact { FirstName = "Test", LastName = "Person", Phone = "123-123-1234", Email = "test#test.com" }).ToArray();
Stopwatch sw = new Stopwatch();
long m1Time = 0;
var total1 = 0;
sw.Start();
foreach (var item in data)
{
var a = ToSyncDictionary(item);
total1++;
}
sw.Stop();
m1Time = sw.ElapsedMilliseconds;
sw.Reset();
long m2Time = 0;
var total2 = 0;
sw.Start();
foreach (var item in data)
{
var a = ToSyncDictionary2(item);
total2++;
}
sw.Stop();
m2Time = sw.ElapsedMilliseconds;
Console.WriteLine($"ToSyncDictionary : {m1Time} for {total1}");
Console.WriteLine($"ToSyncDictionary2 : {m2Time} for {total2}");
Console.ReadLine();
}
public static IDictionary<string, string> ToSyncDictionary<T>(T value)
{
var syncProperties = from p in typeof(T).GetProperties()
let name = p.GetCustomAttribute<SyncProperty>()?.PropertyName
where name != null
select new
{
Name = name,
Value = p.GetValue(value)?.ToString()
};
return syncProperties.ToDictionary(p => p.Name, p => p.Value);
}
public static IDictionary<string, string> ToSyncDictionary2<T>(T value)
{
return Mapper<T>.ToSyncDictionary(value);
}
public static class Mapper<T>
{
private static readonly Func<T, IDictionary<string, string>> map;
static Mapper()
{
map = ObjectSerializer();
}
public static IDictionary<string, string> ToSyncDictionary(T value)
{
return map(value);
}
private static Func<T, IDictionary<string, string>> ObjectSerializer()
{
var type = typeof(Dictionary<string, string>);
var param = Expression.Parameter(typeof(T));
var newExp = Expression.New(type);
var addMethod = type.GetMethod(nameof(Dictionary<string, string>.Add), new Type[] { typeof(string), typeof(string) });
var toString = typeof(T).GetMethod(nameof(object.ToString));
var setData = from p in typeof(T).GetProperties()
let name = p.GetCustomAttribute<SyncProperty>()?.PropertyName
where name != null
select Expression.ElementInit(addMethod,
Expression.Constant(name),
Expression.Condition(Expression.Equal(Expression.Property(param, p), Expression.Constant(null)),
Expression.Call(Expression.Property(param, p), toString),
Expression.Constant(null,typeof(string))));
return Expression.Lambda<Func<T, IDictionary<string, string>>>(Expression.ListInit(newExp, setData), param).Compile();
}
}
On my machine I got a 10x boost in pers.
If you can use some serializer like JSON.net since you will need to change a lot of things to make it work well and you already have a tone of stuff that dose it for you.
I have a map configuration:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<IDictionary<int, MyType1>, List<MyType1>>().ConstructUsing(
x0 => x0?.OrderBy(x => x.Key).Select(x => x.Value).ToList());
});
How can I change this to work with "all MyTypes"?
I want to convert from IDictionary<int, T> to List<T> for any T type.
I found a very ugly solution:
cfg.CreateMap(typeof(IDictionary<,>), typeof(List<>)).ConstructUsing(
x0 =>
{
if (x0 == null)
{
return null;
}
var dict = (IEnumerable)x0;
var type = x0.GetType().GetGenericArguments()[1];
var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(type));
CastDictionaryEntry(dict).OrderBy(x => x.Key).ForEach(x => list.Add(x.Value));
return list;
}
);
and:
static IEnumerable<DictionaryEntry> CastDictionaryEntry(IEnumerable dict)
{
foreach (var item in dict)
{
yield return (DictionaryEntry)item;
}
}
Is there any easier way to do this?
I think works, but then again I haven't tested against a broad set of inputs-
class ABC
{
public int MyProperty { get; set; }
public int MyProperty2 { get; set; }
}
static List<T> ConfigureMap<T>(Dictionary<int, T> input) where T : class
{
AutoMapper.Mapper.Initialize(cfg => cfg.CreateMap(input.GetType(), typeof(List<T>))
.ConstructUsing(Construct));
return Mapper.Map<List<T>>(input);
}
private static object Construct(object arg1, ResolutionContext arg2)
{
if (arg1 != null)
{
var val = arg1 as IDictionary;
var argument = val.GetType().GetGenericArguments()[1];
if (val != null)
{
var type = Activator.CreateInstance(typeof(List<>).MakeGenericType(argument));
foreach (var key in val.Keys)
{
((IList)type).Add(val[key]);
}
return type;
}
}
return null;
}
var so = new Dictionary<int, ABC> { { 1, new ABC { MyProperty = 10, MyProperty2 = 30 } } };
var dest = new List<ABC>();
var res = ConfigureMap<ABC>(so);