Introduction
In this tip we gonna build a custom AutoMap builder that will configure a mapper instance based on custom attributes instead of profiles.
Background
Recently in our project, we had to map many objects from an old version to the new pdo’s and because of refactoring making profiles for each object type wasn’t the best idea.
In order to achieve the following goal “The Less Time Spent, The more work done” we decided to use a custom attribute for mapping source to destination objects so at any time when we had to swap classes we just need to replace attribute for the class.
Using the code
Source example available on github:
So, first of all, let’s take a look at example models
public class Source
{
public string CustomerName { set; get; }
public int Salary { set; get; }
public string WorkAddress { set; get; }
}
[MapTarget(typeof(Source))]
public class Destination
{
public string CustomerName { set; get; }
public int Salary { set; get; }
[MapFieldName("Address","WorkAddress")]
public string Address { set; get; }
}
As you can see they are pretty much similar except one field, we will get back to it later.
Now let’s make couple custom attributes that we are gonna use to build our mapper
[AttributeUsage(AttributeTargets.Class)]
public class MapTargetAttribute : Attribute
{
public Type Source;
public MapTargetAttribute(Type source)
{
Source = source;
}
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class MapFieldName : Attribute
{
public string Name { get; }
public string From { get; }
public MapFieldName(string ToField, string FromField)
{
Name = ToField;
From = FromField;
}
}
The first Attribute will be used to mark a class as Target For the Source object
The Second Attribute optional and might be used for the fields with different names.
Now we have attributes time to build Custom Mapper Builder
public interface IMapperFactory<T1>
{
IMapper BuildMapper(Type targetExplicit = null);
}
public class AutoMapperAttributeBuilder<T1>
: IMapperFactory<T1> where T1 : class
{
public AutoMapperAttributeBuilder()
{
}
public IMapper BuildMapper(Type targetExplicit = null)
{
var types = Assembly.GetExecutingAssembly().GetTypes()
.Union(Assembly.GetCallingAssembly().GetTypes()).AsQueryable();
types = types.Where(t => t.IsClass && t.GetCustomAttributes<MapTargetAttribute>().Count() > 0)
.Where(t => t.GetCustomAttribute<MapTargetAttribute>().Source == typeof(T1));
if (targetExplicit != null)
types = types.Where(e => e.GetType() == targetExplicit);
if (types.Count() == 0)
throw new InvalidOperationException("Target Type Not Found For given source");
var target = types.FirstOrDefault();
var map = new MapperConfiguration(x => x.CreateMap(typeof(T1), target));
if (target.GetProperties().Any(p => p.GetCustomAttributes<MapFieldName>().Count() > 0))
{
var expression = new MapperConfigurationExpression();
var exp = expression.CreateMap(typeof(T1), target);
foreach (var pro in target.GetProperties().Where(p => p.GetCustomAttributes<MapFieldName>().Count() > 0))
{
foreach (var attrDescriptor in pro.GetCustomAttributes<MapFieldName>())
{
var sourceField = attrDescriptor.From;
var targetField = attrDescriptor.Name;
exp.ForMember(targetField, m => m.MapFrom(sourceField));
}
}
map = new MapperConfiguration(expression);
}
return map.CreateMapper();
}
}
Here we are using reflection and MapperConfigurationExpression
in order to make magic happen.
And finally, let's make an extension so we can get an instance to our builder in a more convenient way
public static class TypeEx
{
public static IMapper GetTypeMapper<T>(this Type type) where T : class
{
var builder = new AutoMapperAttributeBuilder<T>();
var mapper = builder.BuildMapper();
return mapper;
}
}
Test and Run
static void Main(string[] args)
{
Console.WriteLine(" AutoMapper CustomAttributes Example ");
var source = new Source {
CustomerName = "test",
Salary = 5000,
WorkAddress = "Some address"
};
var mapper = typeof(Source).GetTypeMapper<Source>();
var target = mapper.Map<Destination>(source);
Console.WriteLine($"{target.CustomerName}");
Console.WriteLine($"{target.Salary}");
Console.WriteLine($"{target.Address}");
Console.ReadLine();
}