Hierarchy of preferences for property in Automapper - c#

Is it possible to set up automapper so that if the result of a MapFrom lambda is null/throws a NRE it will look at a different source. My current requirement is very basic so I've been able to do this:
.ForMember(a=>a.CountryOfRisk, m=>m.MapFrom(a=>(a.CountryOfRisk??a.Issuer.CountryOfRisk).CountryCode))
But really what I'd like to be able to do is something like this:
a=>a.CountryOfRisk, m=>m.MapFrom(a=>a.CountryOfRisk.CountryCode)
.Coalesce(a=>a.Issuer.CountryOfRisk.CountryCode))
This would mean that I could create a whole set of items which would fill the output field in a nice, config based, easy to read formatted.

It is possible to do this custom resolver:
public class CustomResolver : ValueResolver<Source, string>
{
protected override string ResolveCore(Source source)
{
return source.CountryOfRisk ?? source.Issuer.CountryOfRisk;
}
}
Mapper.Initialize(cfg =>
cfg.CreateMap<Source, Destination>()
.ForMember(dest => dest.CountryOfRisk, opt => opt.ResolveUsing<CustomResolver>());
https://github.com/AutoMapper/AutoMapper/wiki/Custom-value-resolvers
(IValueResolver deprecated? ValueResolver should work).

Related

Automapper custom value resolver reuse for multiple types

I have a project which I am trying to use AutoMapper to map from multiple classes in each of these classes there are properties where I would like to use some custom logic to parse the source value to the destination.
I have tried to use custom resolver methods as documented on the AutoMapper docs.
Here is my code:
public class CustomDateTextHandler : IValueResolver<object, object, string>
{
public string Resolve(object source, object destination, string destMember, ResolutionContext context)
{
string txt = source.ToString();
txt.Replace("AM/PM", "tt");
txt.Replace("HH:MM", "hh:mm");
if (txt.Contains("format"))
{
txt.Replace("mmm", "MMM");
}
return txt;
}
}
public class SMapping : Profile
{
public SMapping()
{
CreateMap<SourceForm, s_form>()
.ForMember(dest => dest.id, opt => opt.Ignore())
.ForMember(dest => dest.cell_text, opt => opt.MapFrom<CustomDateTextHandler>())
.ForMember(dest => dest.fn_def, opt => opt.MapFrom<CustomCodeTextResolver>());
}
What I am trying to get is the cell_text value processed with my replace logic in the resolver method but the issue I am facing is that what is being passed to the resolver is the entire SMapping instance, I would like to be able to reuse the resolver code across different classes where the property names will be different, however looking at what it going on at the moment I could not really use the resolver code across my different classes.
Can someone help me?
Thank you in advance.
Use IMemberValueResolver instead of IValueResolver.
Compared to IValueResolver, its Resolve function gets one more parameter: value.
Registering mapping with IMemberValueResolver requires you to pass 1 extra parameter - not the 'value' directly, but a lambda that will produce a 'value' from given source object.
public class CustomDateTextHandler :
IMemberValueResolver< // note: different interface
object, object,
string, string // note: 1 more parameter
>
{
public string Resolve(
object source, object destination,
string sourceValue, string destMember, // note: 1 more parameter
ResolutionContext context
)
{
// here, see the difference:
// source - source object, whole
// sourceMember - value produced by extra lambda passed in mapping
}
}
public class SMapping : Profile
{
public SMapping()
{
CreateMap<SourceForm, s_form>()
...
.ForMember(
dest => dest.cell_text,
opt => opt.MapFrom<CustomDateTextHandler, string>(source => source.PROPERTY11)) // note: this produces that sourceValue
.ForMember(
dest => dest.fn_def,
opt => opt.MapFrom<CustomCodeTextResolver, string>(source => source.PROPERTY22)); // note: this produces that sourceValue
}

How to map empty strings as null with automapper?

I need to map empty strings from source model as null to destination model.
At first I used next profile for this:
public class MyProfile:Profile
{
public MyProfile()
{
CreateMap<SrcModel, DestModel>()
.ForMember(dst => dst.Field1, opt =>
{
opt.Condition(src => !string.IsNullOrEmpty(src.src_Field1));
opt.MapFrom(src => src.src_Field1)
})
//.......
//same for other 15 fields
}
}
But duplicating same logic looks not very good and it's hard to modify it.
Also I have tried to create special map for string like this:
CreateMap<string, string>().ConvertUsing(src => string.IsNullOrEmpty(src) ?
null : src)
But such string map has impact on all my maps, but I need such logic only for several maps, not for all.
I also have tried to use ForAllMembers method:
... .ForAllMembers(opt => opt.Condition();
But there is no way do define type of source member, to cpecify some condition for strings.
What is the best way to define some common mapping logic for several members of same type for one map?
Just duplicate the logic, the most I'd do is extract the Condition part into an extension method you can call.

AutoMapper objects with different property types

I want to map my Entity Framework entities (generated from a legacy database) to custom DTO objects (which should be nice and clean).
My legacy DB has entities looking a bit like this:
internal class Order {
int id;
string Shipping_date;
string quantity;
}
And I want to map it to a nicer DTO object:
public class OrderDto {
int id;
DateTime? ShippingDate;
int Quantity;
}
I have written an "entity container" to provide dependency injection, which returns values this way:
public IEnumerable<OrderDto> GetPaginatedOrders(int page, int pageSize)
{
return this.db.Orders
.OrderByDescending(c => c.id)
.Paginate(page, pageSize)
.Project()
.To<OrderDto>()
.AsEnumerable();
}
So: change of types, and change of property names.
Were it only change of property names, it would be easy-but-tedious:
Mapper.CreateMap<Order, OrderDto>()
.ForMember(dest => dest.Quantity, opt => opt.MapFrom(src => src.quantity))
.ForMember(dest => dest.ShippingDate, opt => opt.MapFrom(src => src.Shipping_date));
This is not enough with type changes. I tried a whole bunch of stuff:
Parsing the properties at the mapping declaration, like src => int.Parse(src.quantity) but Linq doesn't like it.
Extending the EF entities with custom properties like QuantityInt { get { return int.Parse(this.quantity) } } and using these in the mapping, but AutoMapper doesn't like it, and explicitly don't support them.
Mapping system types one to another like Mapper.CreateMap<string, int>().ConvertUsing(Convert.ToInt32) but I still get Unable to create a map expression from System.String to System.Int32 errors.
Using custom converters for my class, but I always get empty values from ResolutionContext.SourceValues at run-time from my entities (I'm guessing that they are disposed before AutoMapper gets them or something like this).
I'm realizing that AutoMapper is convention-based, so maybe I should use another tool, but which one exist?
Thanks for your help!
.Project() uses Linq to entities, which generates SQL and naturally only understands a very limited set of functions.
If you use
Mapper.Map<IEnumerable<Order>, IEnumerable<OrderDto>>(src)
your conversions will work fine.

Automapper - can it map over only existing properties in source and destination objects?

I have a simple update function:
public void Update(Users user)
{
tblUserData userData = _context.tblUserDatas.Where(u => u.IDUSER == user.IDUSER).FirstOrDefault();
if (userData != null)
{
Mapper.CreateMap<Users, tblUserData>();
userData = Mapper.Map<Users, tblUserData>(user);
_context.SaveChanges()
}
}
userData is an EF entity, and it's Entity Key property gets nulled out because, I believe, it exists in the destination object, but not in the source object, so it gets mapped over with its default value (for the Entity Key, that's null)
So, my question is can Automapper be configured to only attempt to map the properties that exist in both the source and destination objects? I'd like things like the Entity Key and navigation properties to be skipped over.
You can explicitly tell AutoMapper to Ignore certain properties if required like this:
Mapper.CreateMap<Users, tblUserData>()
.ForMember(dest => dest.Id, opt => opt.Ignore());
which will mean the Id column in the destination object will ALWAYS be left untouched.
You can use the Condition option to specify whether or not a mapping is applied depending on the result of a logical condition, like this:
Mapper.CreateMap<Users, tblUserData>()
.ForMember(dest => dest.Id, opt => opt.Condition(src=>src.Id.HasValue));
or
Mapper.CreateMap<Users, tblUserData>()
.ForMember(dest => dest.Id, opt => opt.Condition(src=>src.Id != null));
depending on your specific requirements.
Instead of
userData = _mapper.Map<Users, tblUserData>(user);
use
_mapper.Map(user, userData);
The former creates a new userData object with only the properties available in user object. The later overwrites the existing userData object's properties with properties present in user object, while retaining the rest.
You can tell AutoMapper to ignore fields that you do not want mapped like this:
userData = Mapper.Map<Users, tblUserData>(user).ForMember(m => m.EntityKey, opt => opt.Ignore());
You can override this behavior by making a small extension method to ignore all properties that do not exist on the target type.
public static IMappingExpression<TSource, TDestination> IgnoreAllNonExisting<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
{
var sourceType = typeof(TSource);
var destinationType = typeof(TDestination);
var existingMaps = Mapper.GetAllTypeMaps().First(x => x.SourceType.Equals(sourceType)
&& x.DestinationType.Equals(destinationType));
foreach (var property in existingMaps.GetUnmappedPropertyNames())
{
expression.ForMember(property, opt => opt.Ignore());
}
return expression;
}
Then it is possible to do the mapping as follows:
Mapper.CreateMap<SourceType, DestinationType>().IgnoreAllNonExisting();
It is also possible to customize this method to your needs, by specifically ignoring properties which have a protected or private setter, for example.

Can AutoMapper be "persuaded" to temporarily suspend particular mappings?

Can AutoMapper be "persuaded" to temporarily suspend particular mappings?
To illustrate what am trying to accomplish, I will use an illustration. Suppose that I have a repository, StudentRepository, that uses LINQ to interacts with database objects (tables) like Students, Courses, Activities, Clubs etc. On the application side, there are matching domain objects Student, Course, Activity, Club. The Student class contains array members of type Course, Activity, and Club like:
public class Student
{
// ... more members
public Course[] Courses { get; set; }
public Activity[] Activities { get; set; }
public Club[] Clubs { get; set; }
// ... even more members
}
AutoMapper is configured to map the database objects to the domain objects where the mappings are defined in a static constructor of StudentRepository like:
public class StudentRepository : IStudentRepository
{
static class StudentRepository
{
// ... other mappings
Mapper.CreateMap<TableStudent, Student>()
.ForMember(dest => dest.Courses, opt => opt.MapFrom(src => Mapper.Map<IEnumerable<Course>>(src.TableCourses)))
.ForMember(dest => dest.Activities, opt => opt.MapFrom(src => Mapper.Map<IEnumerable<Activity>>(src.TableActivities)))
.ForMember(dest => dest.Clubs, opt => opt.MapFrom(src => Mapper.Map<IEnumerable<Clubs>>(src.TableClubs)))
// where TableStudents, TableCourses, TableActivities, TableClubs are database entities
// ... yet more mappings
}
}
Is it possible to "persuade" AutoMapper to suspend the mappings within one function block? For example:
public Student[] GetStudents()
{
DataContext dbContext = new StudentDBContext();
var query = dbContext.Students;
// => SUSPEND CONFIGURATION MAPPINGS for Subjects, Activities and Clubs WHILE STILL making use of others
// => The idea here it to take personal charge of 'manually' setting the particular members (*for some specific reasons)
var students = Mapper.Map<Student>(query); // => Still be able to use AutoMapper to map other members
}
public Student[] OtherStudentRepositoryMethods()
{
// Other repository methods continue to make use of the mappings configured in the static constructor
}
NOTE "for some specific reasons": One reason one may want to take control away from AutoMapper would be this http://codebetter.com/davidhayden/2007/08/06/linq-to-sql-query-tuning-appears-to-break-down-in-more-advanced-scenarios/ where in the case of a 1:n associations, LINQ to SQL only supports joining-in one 1:n association per query. AutoMapper would be inefficient here - making N calls to load Courses for N students returned, N more calls to load Activities for the same N students returned, and possibly N more calls to load Clubs for the same N students returned.
Is it possible to "persuade" AutoMapper to suspend the mappings within one function block?
As I know, the best way to do it - use Ignore() like this
public class StudentRepository : IStudentRepository
{
static class StudentRepository
{
// ... other mappings
Mapper.CreateMap<TableStudent, Student>()
.ForMember(dest => dest.Courses, opt => opt.Ignore())
.ForMember(dest => dest.Activities, opt => opt.Ignore())
.ForMember(dest => dest.Clubs, opt => opt.Ignore())
// where TableStudents, TableCourses, TableActivities, TableClubs are database entities
// ... yet more mappings
}
}
Also, as it was noticed before, I'd recommend you to use different profiles for each goal you want to achieve.Here is a example
public BaseService()
{
AutoMapperRegistry.Configure();
}
public class AutoMapperRegistry
{
public static void Configure()
{
Mapper.Initialize(x =>
{
x.AddProfile<ServiceProfile1>();
x.AddProfile<ServiceProfileReverseProfile1>();
});
}
}
public class ServiceProfile1 : Profile
{
protected override string ProfileName
{
get
{
return "ServiceProfile1";
}
}
protected override void Configure()
{
Mapper.CreateMap<DataContract_Sub, DTO_Sub>();
Mapper.CreateMap<DataContract, DTO>()
.ForMember(x => x.DataContract_Sub, opt => opt.MapFrom(y => y.DTO_Sub))
.BeforeMap((s, d) =>
{
// your custom logic
})
.AfterMap((s, d) =>
{
// your custom logic
});
}
}
One way of achieving this would be to create separate mapping engine instances for each scenario, that way you could configure different maps, as suggested in this answer from Jimmy Bogard on wanting to map a single type in different ways.
hmm... Thanks guys for the feedback. I took time to consider all answers and suggestions. None particularly renders exactly well though they provided a lot of food for thought. I thought I should inject something I tried. (Disclaimer: My opinion is that its a dirty approach - many things could go wrong - and Murphy's laws continue to hold). You could leverage the Ignore functionality in the particular instance to "suspend" the mapping. Typically, in a try and catch block as follows:
public Student[] GetStudents()
{
try
{ // Suspend/Ignore the mappings
// => SUSPEND CONFIGURATION MAPPINGS for Subjects, Activities and Clubs
Mapper.CreateMap<TableStudent, Student>()
.ForMember(dest => dest.Courses, opt => opt.Ignore())
.ForMember(dest => dest.Activities, opt => opt.Ignore())
.ForMember(dest => dest.Clubs, opt => opt.Ignore())
DataContext dbContext = new StudentDBContext();
var query = dbContext.Students;
// other logic ...
var students = Mapper.Map<Student>(query); // => Still be able to use AutoMapper to map other members
// set the properties you needed to do manually
}
finally // Restore back the mappings
{
Mapper.CreateMap<TableStudent, Student>()
.ForMember(dest => dest.Courses, opt => opt.MapFrom(src => Mapper.Map<IEnumerable<Course>>(src.TableCourses)))
.ForMember(dest => dest.Activities, opt => opt.MapFrom(src => Mapper.Map<IEnumerable<Activity>>(src.TableActivities)))
.ForMember(dest => dest.Clubs, opt => opt.MapFrom(src => Mapper.Map<IEnumerable<Clubs>>(src.TableClubs)))
}
}
Like I mentioned, its perhaps dirty. Not the kind of code I would be happy wriing - especially since I don't know what kind of exceptional situations can arise if CreateMap() fails within the finally block, but on a legacy application where you couldn't overhaul the approach - to possibly use different profiles like suggested by #AndriyZakharko above, you could use it to get control back temporarily. I tried it out personally.

Categories

Resources