Global/Shared mapping configuration: custom mappings & ignores - c#

I'm working with .NET Core 2.0 and AutoMapper 6.2.2.
I'm using AssertConfigurationIsValid to make sure my mappings are correct and I have defined a number of AutoMapper.Profile which share some mapping rules:
Since I'm mapping between model entities and DTOs, I want field id
to be ignored in every entity <=> DTO mapping.
I also want AutoGeneratedAppid to be mapped to AppId in every
entity <=> DTO mapping.
For instance:
public class Role
{
public long Id { get; set; }
public string AutoGeneratedAppId { get; set; }
public string Name { get; set; }
}
public class RoleDTO
{
public string AppId { get; set; }
public string Name { get; set; }
}
public class RoleProfile : AutoMapper.Profile
{
public RoleProfile()
{
CreateMap<Role, RoleDTO>();
CreateMap<RoleDTO, Role>();
//.ForMember(entity => entity.Id, opt => opt.Ignore())
//.ForMember(entity => entity.AutoGeneratedAppId, opt => opt.MapFrom(dto => dto.AppId));
}
}
Since there both multiple mappings and multiple fields to be ignored/mapped in the same way in each mapping, I'm trying to found a way of avoiding having to define in every profile common rules this like:
.ForMember(entity => entity.Id, opt => opt.Ignore());
.ForMember(entity => entity.AutoGeneratedAppId, opt => opt.MapFrom(dto => dto.AppId));
I can't find a way to avoid a AutoMapperConfigurationException in AssertConfigurationIsValid when mapping dto => entity due to unmapped id and AutoGeneratedAppId properties.
My mapper configuration, with my failed trials, is the following:
protected override MapperConfiguration CreateConfiguration()
{
var config = new MapperConfiguration(cfg =>
{
cfg.DisableConstructorMapping();
cfg.AddProfiles(Assemblies);
// Global ignoring - alternative 1
cfg.ShouldMapProperty = prop =>
prop.Name != "Id";
// Global ignoring - alternative 2
cfg.AddGlobalIgnore("Id");
// Global ignoring - alternative 3
cfg.ForAllPropertyMaps(map =>
map.SourceMember.Name.EndsWith("Id"),
(map, configuration) =>
{
configuration.Ignore();
});
// Entity.AutoGeneratedAppId => DTO.AppId
cfg.RecognizePrefixes("AutoGenerated");
// DTO.AppId => Entity.AutoGeneratedAppId
cfg.RecognizeDestinationPrefixes("AutoGenerated");
});
return config;
}
My Startup configuration is the following:
var mapperProvider = new MapperProvider();
services.AddSingleton<IMapper>(mapperProvider.GetMapper());
services.AddAutoMapper(mapperProvider.Assemblies);
Thanks in advance.

Related

How can I include a child object mapping using AutoMapper when the source objects have no inheritance relationship?

I have an object model something like this:
public class Concert {
public Band Band { get; set; }
public ConcertVenue Venue { get; set; }
}
public class TicketOrder {
public Concert Concert { get; set; }
public string CustomerName { get; set; }
}
// DTOs for email and web views:
public class ConcertDto {
public string Artist { get; set; }
public string Venue { get; set; }
}
public class TicketOrderDto : ConcertDto {
public string CustomerName { get; set; }
}
I'm using AutoMapper to map domain objects to DTOs. The DTOs here have an inheritance relationship that doesn't exist in the domain model (because when I send an email about a ticket order, I want to include all the information about the concert)
I have a mapping defined like this:
config.CreateMap<Concert, ConcertDto>()
.ForMember(dto => dto.Artist, opt => opt.MapFrom(concert => concert.Band.Name))
.ForMember(dto => dto.Venue, opt => opt.MapFrom(concert => concert.GetVenueSummary());
config.CreateMap<TicketOrder, ConcertDto>()
.ForMember(dto => dto.Artist, opt => opt.MapFrom(concert => concert.Band.Name))
.ForMember(dto => dto.Venue, opt => opt.MapFrom(concert => concert.GetVenueSummary())
.ForMember(dto => dto.CustomerName, optn.MapFrom(order => order.Customer.FullName))
;
There's some duplication in those maps, and what I want to do is to reuse the Concert > ConcertViewData mapping when I map the TicketOrderDto:
cfg.CreateMap<TicketOrder, TicketOrderDto>()
// This is what I *want* but isn't valid AutoMapper syntax:
.IncludeMembers(dto => dto, order => order.Concert)
.ForMember(dto => dto.CustomerName, optn.MapFrom(order => order.Customer.FullName));
but this fails with:
System.ArgumentOutOfRangeException: Only member accesses are allowed.
dto => dto (Parameter 'memberExpressions')
at AutoMapper.ExpressionExtensions.EnsureMemberPath(LambdaExpression exp, String name)
Calling .IncludeBase<> doesn't work, because ConcertOrder doesn't derive from Concert.
Is there an easy way to import one map into another but specify that it should map from a child object of the source type? i.e. "hey, please map source.Child onto this, and then run the regular source > this mapping?"
I am going to make an assumption here, but I believe the mapping should be from TicketOrder to TicketOrderDto, and not ConcertDto (which contains no CustomerName property) as the given models don't match the given mapping configuration.
In that case, you should be able to use .AfterMap() on ticket mapper configuration to map from Concert to ConcertDto.
cfg.CreateMap<TicketOrder, TicketOrderDto>()
.ForMember(d => d.CustomerName, o => o.MapFrom(s => s.Order.Customer.Name))
.AfterMap((s, d, context) => context.Mapper.Map(s.Concert, d));

Why doesn't Automapper inherit ForPath-ignores?

We are mapping domain hierarchy to the Dto hierarchy. Some of the properties of the domain base class get flattened. We are using ReverseMap to simplify the mapping back to domain from dto.
We've mapped Contained.Id do ContainedId of Dto and ignored the path for the reverse map.
What we were expecting, that mapping back to domain object shouldn't create new instances for Contained-property. But that was not the case.
After some investigations we've found out, that it was due to not-inheriting the .ForPath(, opt => opt.Ignore()) for the ReverseMap.
We are using 9.0.0 version of Automapper.
Here is the code:
public class Contained {
public Guid Id { get; set; }
}
public class Base {
public Contained Contained { get; set;}
}
public class Derived: Base { }
public abstract class BaseDto {
public Guid? ContainedId { get; set; }
}
public class DerivedDto: BaseDto { }
[Test]
public void ForPathInheritanceWorks()
{
var configuration = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Base, BaseDto>()
.IncludeAllDerived()
.ReverseMap()
.ForPath(d => d.Contained.Id, opt => opt.Ignore());
cfg.CreateMap<Derived, DerivedDto>()
.ReverseMap();
});
var mapper = configuration.CreateMapper();
var derived = mapper.Map<Derived>(new DerivedDto());
Assert.That(derived.Contained, Is.Null); // fails, because derived.Contained is not null.
}
To workaround an issue we had to add
.ForPath(d => d.Contained.Id, opt => opt.Ignore())
after ReverseMap of the derived class.
For me it looks like a bug in Automapper.

To map related entity property to a viewmodel property using Automapper

I have User Table, UserParents table, UserMarks table and UserGrades Table. I am trying to use automapper to map a few of the properties to my view model.
The User table Model :
public partial class User
{
public string UserId { get; set; }
public string UserName { get; set; }
public virtual ICollection<UserParents> UserParents { get; set; }
public virtual ICollection<UserMarks> UserMarks { get; set; }
public virtual ICollection<UserGrades> UserGrades { get; set; }
}
My ViewModel: This contains a portion of the fields from each of the four table.
public class UserViewModel
{
public string UserId{get;set;}
//From UserParents table
public string UserParentName{get;set;}
}
My query :
var user = context.User
.Include(i => i.UserParents)
.Include(i => i.UserMarks)
.Include(i => i.UserGrades)
.Where(i =>i.userId == userId).FirstOrDefault();
And automapper:
config = new MapperConfiguration(cfg => {
cfg.CreateMap<User,UserViewModel>()
//This works
.ForMember(m => m.UserId,opt =>opt.MapFrom(entity => entity.UserId))
//Can't map vm.UserParentName directly to entity.UserParents.UserParentName and so have to do all of this
.ForMember(vm => vm.UserParentName, opt => opt.MapFrom(entity => entity.UserParents.Select(c =>c.UserParentName).FirstOrDefault()))
.ReverseMap();});
IMapper mapper = config.CreateMapper();
So as in the commented portion of the code, why can't I directly map vm.UserParentName directly to entity.UserParents.UserParentName ?
Is there any other way of doing it?
Change your configuration like so:
config = new MapperConfiguration(cfg => {
cfg.CreateMap<User,UserViewModel>()
//This is actually unnecesseray
//.ForMember(m => m.UserId,opt =>opt.MapFrom(entity => entity.UserId))
// If you only want the first parent name - Not sure on structure of UserParent class so just assuming you have a field "Name"
.ForMember(vm => vm.UserParentName,
opt => opt.MapFrom(entity => entity.UserParents.FirstOrDefault().Name))
.ReverseMap();
});
IMapper mapper = config.CreateMapper();
The map betwen m.UserId and entity.User Id is not needed, Automapper will do this automatically.
The map for UserParentName, I'm not exactly sure why you would want to get the first in the list of them, but if that is definitely the case then just use the code above to fetch it.

Can AutoMapper mappings be composed?

The models I'm working with include an entry object which I'd like to map as if its child object were the entire object.
Here is a simplified version of the problem. I'd like an instance of OurWrappedSource to map directly to OurTarget.
class OurTarget
{
public Guid Id { get; set; }
public string Notes { get; set; }
public int Quantity { get; set; }
}
class OurSource
{
public Guid Id { get; set; }
public string Notes { get; set; }
public int Quantity { get; set; }
}
class OurWrappedSource
{
public OurSource Source { get; set; }
}
private static void TestUnwrapUsingConfig(MapperConfiguration config)
{
config.AssertConfigurationIsValid();
IMapper mapper = new Mapper(config);
var wrappedSource = new OurWrappedSource
{
Source = new OurSource
{
Id = new Guid("123e4567-e89b-12d3-a456-426655440000"),
Notes = "Why?",
Quantity = 27
}
};
var target = mapper.Map<OurTarget>(wrappedSource);
Assert.Equal(wrappedSource.Source.Id, target.Id);
Assert.Equal(wrappedSource.Source.Notes, target.Notes);
Assert.Equal(wrappedSource.Source.Quantity, target.Quantity);
}
The following configuration works, but is unwieldy for more than a couple of members:
// Works, but isn't *auto* enough
TestUnwrapUsingConfig(new MapperConfiguration(cfg =>
{
cfg.CreateMap<OurWrappedSource, OurTarget>()
.ForMember(src => src.Id, opts => opts.MapFrom(wrappedSource => wrappedSource.Source.Id))
.ForMember(src => src.Notes, opts => opts.MapFrom(wrappedSource => wrappedSource.Source.Notes))
.ForMember(src => src.Quantity, opts => opts.MapFrom(wrappedSource => wrappedSource.Source.Quantity));
}));
What I'd like to be able to do is define two intermediate mappings an then compose them:
Map OurWrappedSource directly to OurSource
Map OurSource directly to OurTarget
Map OurWrappedSource to OurTarget by composing mapping 1 with mapping 2
After some hammering, I have this configuration:
// Works, but #3 probably isn't ProjectTo-friendly
TestUnwrapUsingConfig(new MapperConfiguration(cfg =>
{
// 1
cfg.CreateMap<OurWrappedSource, OurSource>()
.ConvertUsing(wrappedSource => wrappedSource.Source);
// 2
cfg.CreateMap<OurSource, OurTarget>();
// 3
cfg.CreateMap<OurWrappedSource, OurTarget>()
.ConstructUsing((wrappedSource, ctx) =>
ctx.Mapper.Map<OurTarget>(ctx.Mapper.Map<OurSource>(wrappedSource))
)
.ForAllOtherMembers(opts => opts.Ignore());
}));
This works exactly as specified, but mapping 3 seems perhaps a little more explicit and/or kludgey than it should. It involves code in a Func (rather than an expression), which makes me think it probably won't optimize well when used with ProjectTo(). Is there a way to rewrite mapping 3 to address these issues?

how do I map this using Automapper

I am in need to map the below scenario.
public class Customer
{
public string CustomerJson { get; set; }
}
public class CustomerTO
{
public object CustomerJson { get; set; }
}
From DAL I get CustomerJson value as below.
Customer.CustomerJson = {
"name": "Ram",
"city": "India"
}
I am in need to Deserialize this string. so I tried the below stuff while mapping.
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Customer, CustomerTO>()
.ForMember(dest => dest.CustName, opt => opt.MapFrom(src => JsonConvert.DeserializeObject(src.CustName)));
});
But this gives me run time error.
Unhandled Exception: AutoMapper.AutoMapperMappingException: Error mapping types.
So I kept it simple.
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Customer, CustomerTO>()
.ForMember(dest => dest.CustName, opt => opt.MapFrom(src => (src.CustName));
});
And I tried to deserialize it while consuming. But this give compile time error.
var custJson = JsonConvert.DeserializeObject(customerTO.CustomerJson );
Error 2 The best overloaded method match for 'Newtonsoft.Json.JsonConvert.DeserializeObject(string)' has some invalid arguments
I know customerTO.CustomerJson is not string but how do should I do the required mapping?
Thanks.
Based on your previous question and given the information above you seem to be confusing what you're trying to do here.
So I'm going to amalgamate the data from both in an attempt to solve the issues.
Entity Classes:
public class Customer
{
public int CustomerId {get; set; }
public string CustomerName { get; set; }
public string CustomerJson { get; set; }
}
public class CustomerTO
{
public int CustId { get; set; }
public object CustData { get; set; }
public object CustomerJson { get; set; }
}
AppMapper Class:
public static class AppMapper
{
public static MapperConfiguration Mapping()
{
return new MapperConfiguration(cfg =>
{
cfg.CreateMap<Customer, CustomerTO>()
.ForMember(dest => dest.CustId, opt => opt.MapFrom(src => src.CustomerId))
.ForMember(dest => dest.CustData, opt => opt.MapFrom(src => src.CustName))
.ForMember(dest => dest.CustomerJson, opt => opt.MapFrom(src => JsonConvert.DeserializeObject(src.CustomerJson));
});
}
}
Main:
public class Program
{
static void Main(string[] args)
{
var config = AppMapper.Mapping();
var mapper = config.CreateMapper();
// From Previous question get list of Customer objects
var customers = AddCustomers();
var mappedCustomers = mapper.Map<IEnumerable<CustomerTO>>(customers);
}
}
A couple of things to point out
I'm not sure what the purpose of CustData is in CustomerTO. It seems to be duplicating CustomerJson and if so remove it and the associated mapping.
Also, you never mention mapping from DTO back to entity, but for the JsonObject you just need to configure it to map the serialized string to the appropriate Property.
This is how I addressed my requirement.
Db Entity
public class Customer
{
public string CustomerData { get; set; }
// & other properties
}
My DTO
public class CustomerTO
{
public object CustomerData { get; set;}
// & other properties
}
I created a Utility like class with name AppMapper. This is how my AppMapper.cs looks like.
public class AppMapper
{
private IMapper _mapper;
public AppMapper()
{
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Customer, CustomerTO>();
//& other such mapping
});
_mapper = config.CreateMapper();
}
public CustomerTO Map(Customer customerEntity)
{
var customerTo= _mapper.Map<Customer,CustomerTO>(customerEntity);
return customerTo;
}
Now when I needed the mapping.
class DAL
{
public CustomerTO GetCustomers()
{
var customers= //LINQ To get customers
var customerTO = Mapping(customer);
return customerTO;
}
//However, this mapping glue in some internal class to retain SOLID principles
private CustomerTO Mapping(Customer custEntity)
{
var custTO = _appMapper.Map(custEntity);
var str = JsonConvert.Serialize(custTO.CustomerData);
custTO.CustomerData = JsonConvert.Deserialize(str);
return custTO;
}
}
That's it.
#Barry O'Kane - Sincere thanks for your inputs.
Points to be noted:-
I don't need to map manually any of the properites since the property name is same. Plus I am casting string to object. So no issues.
If you use .Map() for one property, then I found that I need to map each property else it gives default value of the data type.(Ex. for int it gives 0).
Yes. agreed there could be other method in Automapper which allows me specify that for a particulay property do this manual mapping & for rest use Automapping mechanism. But I am not sure on that.
Please feel free to improve this ans in any way.
Hope this helps :)

Categories