Map List of Customers with automapper - c#

Using Automapper I am trying to map one object to another. One property is a class called Task containing a list of customers. The other class is called Result and contains a count of customers as well as another list of customers.
This is my current approach which fills information into the order properties correctly, but fails in result, which is still null. How can I get the List into result? How do i need to change the maps and do i need to create a map in both directions, or this this not necessary?
Mapper.Initialize(cfg =>
{
cfg.CreateMap<CustomerPost.RootObject, Customers.RootObject>();
cfg.CreateMap<CustomerPost.Order, Customers.Order>();
cfg.CreateMap<Customers.Result, CustomerPost.Task>();
cfg.CreateMap<CustomerPost.Task, Customers.Result>()
.ForMember(x => x.customerscount, opt => opt.Ignore())
.ForMember(x => x.customerstotalcount, opt => opt.Ignore());
});
try
{
Mapper.AssertConfigurationIsValid();
}
catch (AutoMapperConfigurationException ex)
{
//TODO: Handle this
throw ex;
}
var customer = Mapper.Map<CustomerPost.RootObject, Customers.RootObject>(input);
here are my current classes (Customer):
public class Result
{
public int customerstotalcount { get; set; }
public int customerscount { get; set; }
public List<Customer> customers { get; set; }
}
public class RootObject
{
public Status status { get; set; }
public Order order { get; set; }
public Result result { get; set; }
}
CustomerPost:
public class Task
{
public List<Customer> customers { get; set; }
}
public class RootObject
{
public Order order { get; set; }
public List<Task> tasks { get; set; }
}

Okay so the solution to my problem was that my mapping didn't find "result" so i've just mapped my RootObject like this:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<CustomerPost.RootObject, Customers.RootObject>()
.ForMember(x => x.status, opt => opt.Ignore())
.ForMember(x => x.order, opt => opt.Ignore())
.ForMember(dest => dest.result, src => src.MapFrom(opt => opt.tasks.FirstOrDefault()));
then i went ahead and just mapped the result like this:
var result = Mapper.Map<CustomerPost.Task, Customers.Result>(input.tasks.FirstOrDefault());
var customer = new Customers.Customer();
customer = result.customers.FirstOrDefault();
and just bound it to a new Customerobject. Then all my information got transmitted as expected

Related

AutoMapper mapping from multiple properties to complex objects fails with ReverseMap and custom value resolver

I have issues on reverse mapping multiple properties back to complex objects, even with custom value resolvers.
Here are the persistence model:
public class EmailDbo
{
public int EmailId { get; set; }
public DateTime DateCreated { get; set; }
public DateTime? DateSent { get; set; }
public string SendTo { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
public bool DownloadAvailable { get; set; }
public DateTime? AdminDateSent { get; set; }
public string AdminEmail { get; set; }
public string AdminSubject { get; set; }
public string AdminBody { get; set; }
public int StatusId { get; set; }
}
I have Dapper map data from database and fill in this model.
Here are the domain models I want to map back and forth with the persistence model:
public class Email
{
public string SendTo { get; private set; }
public string Subject { get; private set; }
public string Body { get; private set; }
public DateTime? DateSent { get; private set; }
public Email(string sendTo, string subject, string body, DateTime? dateSent = null)
{
// Validations
this.SendTo = sendTo;
this.Subject = subject;
this.Body = body;
this.DateSent = dateSent;
}
}
public enum EmailTaskStatus
{
Sent = 1,
Unsent = 2
}
public class EmailTask
{
public int Id { get; private set; }
public DateTime DateCreated { get; private set; }
public Email PayerEmail { get; private set; }
public Email AdminEmail { get; private set; }
public bool DownloadAvailableForAdmin { get; private set; }
public EmailTaskStatus Status { get; private set; }
public EmailTask(int emailTaskId, DateTime dateCreated, Email payerEmail, Email adminEmail,
bool downloadAvailable, EmailTaskStatus status)
{
// Validations
this.Id = emailTaskId;
this.DateCreated = dateCreated;
this.PayerEmail = payerEmail;
this.AdminEmail = adminEmail;
this.DownloadAvailableForAdmin = downloadAvailable;
this.Status = status;
}
}
I would like to use a value object called Email for both the payer and admin email. You can tell they're just stored flatten in the database/persistence model. And the payer email is required but not the admin email.
I have the mapping configured like following:
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<EmailTask, EmailDbo>()
.ForMember(dest => dest.EmailId, opts => opts.MapFrom(src => src.Id))
.ForMember(dest => dest.SendTo, opts => opts.MapFrom(src => src.PayerEmail.SendTo))
.ForMember(dest => dest.Subject, opts => opts.MapFrom(src => src.PayerEmail.Subject))
.ForMember(dest => dest.Body, opts => opts.MapFrom(src => src.PayerEmail.Body))
.ForMember(dest => dest.DateSent, opts => opts.MapFrom(src => src.PayerEmail.DateSent))
.ForMember(dest => dest.DownloadAvailable, opts => opts.MapFrom(src => src.DownloadAvailableForAdmin))
.ForMember(dest => dest.AdminEmail, opts =>
{
opts.PreCondition(src => (src.AdminEmail != null));
opts.MapFrom(src => src.AdminEmail.SendTo);
})
.ForMember(dest => dest.AdminSubject, opts =>
{
opts.PreCondition(src => (src.AdminEmail != null));
opts.MapFrom(src => src.AdminEmail.Subject);
})
.ForMember(dest => dest.AdminBody, opts =>
{
opts.PreCondition(src => (src.AdminEmail != null));
opts.MapFrom(src => src.AdminEmail.Body);
})
.ForMember(dest => dest.AdminDateSent, opts =>
{
opts.PreCondition(src => (src.AdminEmail != null));
opts.MapFrom(src => src.AdminEmail.DateSent);
})
.ForMember(dest => dest.StatusId, opts => opts.MapFrom(src => (int)src.Status))
.ReverseMap()
.ForCtorParam("status", opts => opts.MapFrom(src => src.StatusId))
.ForMember(dest => dest.PayerEmail, opts => opts.MapFrom<PayerEmailValueResolver>())
.ForMember(dest => dest.AdminEmail, opts => opts.MapFrom<AdminEmailValueResolver>());
}
}
After ReverseMap(), I want to grab multiple properties and construct the complex object Email. Hence I define two custom value resolvers for that:
public class PayerEmailValueResolver : IValueResolver<EmailDbo, EmailTask, Email>
{
public Email Resolve(EmailDbo emailDbo, EmailTask emailTask, Email email, ResolutionContext context)
{
return new Email(emailDbo.SendTo, emailDbo.Subject, emailDbo.Body, emailDbo.DateSent);
}
}
public class AdminEmailValueResolver : IValueResolver<EmailDbo, EmailTask, Email>
{
public Email Resolve(EmailDbo emailDbo, EmailTask emailTask, Email email, ResolutionContext context)
{
if (String.IsNullOrWhiteSpace(emailDbo.AdminEmail) &&
String.IsNullOrWhiteSpace(emailDbo.AdminSubject) &&
String.IsNullOrWhiteSpace(emailDbo.AdminBody) &&
!emailDbo.AdminDateSent.HasValue)
{
return null;
}
return new Email(emailDbo.SendTo, emailDbo.Subject, emailDbo.Body, emailDbo.DateSent);
}
}
As always, the mapping from the domain model to the Dbo works fine:
but not the other way, from Dbo to domain model. It's throwing exceptions:
Unhandled exception. System.ArgumentException: Program+EmailTask needs to have a constructor with 0 args or only optional args. (Parameter 'type')
at lambda_method32(Closure , Object , EmailTask , ResolutionContext )
at AutoMapper.Mapper.MapCore[TSource,TDestination](TSource source, TDestination destination, ResolutionContext context, Type sourceType, Type destinationType, IMemberMap memberMap)
at AutoMapper.Mapper.Map[TSource,TDestination](TSource source, TDestination destination)
at AutoMapper.Mapper.Map[TDestination](Object source)
.Net Fiddle demo: https://dotnetfiddle.net/DcTsPG
I wonder if AutoMapper confuses about those two Email objects: payer email and admin email, because they're both are Email type.
In reverse map AutoMapper is failing to create an instance of EmailTask.
Add a parameterless constructor to your EmailTask class -
public EmailTask()
{
// AutoMapper use only
}
Also, since your value resolvers are creating instance of Email, add a parameterless constructor to your Email class too -
public Email()
{
// AutoMapper use only
}
Finally, modify the PayerEmail and AdminEmail properties in EmailTask class so they can be set publicly -
public Email PayerEmail { get; set; }
public Email AdminEmail { get; set; }
That should solve your issue.
EDIT :
#David Liang, after reading your comment I'd say, to suit your scenario in light of DDD, you might need to modify your current mapping approach.
The thing is, when you are mapping EmailDbo from EmailTask, the process is easier because EmailDbo is a DTO type class with no parameterized constructor. Therefore, the property mapping only is enough to do the job.
But when you are trying to map EmailTask from EmailDbo, you are trying to instantiate a domain model class which has very strictly defined parameterized constructor that takes complex types as parameters, and is trying to protect how it's properties can and cannot be accessed from outside. Therefore, the .ReverseMap() approach you are using currently will not be very helpful, because the property mapping only will not be enough to provide you all the constructor parameters needed to instantiate the class. There's also AutoMapper's naming convention in the play.
Following is a mapping configuration for EmailTask from EmailDbo, where the reverse mapping is separated out and the value resolvers are refactored out into a helper class. The forward mapping remained unchanged.
CreateMap<EmailDbo, EmailTask>()
.ConstructUsing((s, d) =>
new EmailTask(
s.EmailId,
s.DateCreated,
Helper.GetPayerEmail(s),
Helper.GetAdminEmail(s),
s.DownloadAvailable,
(EmailTaskStatus)s.StatusId))
.IgnoreAllPropertiesWithAnInaccessibleSetter();
The Helper class -
public class Helper
{
public static Email GetPayerEmail(EmailDbo emailDbo)
{
return new Email(emailDbo.SendTo, emailDbo.Subject, emailDbo.Body, emailDbo.DateSent);
}
public static Email GetAdminEmail(EmailDbo emailDbo)
{
if (string.IsNullOrWhiteSpace(emailDbo.AdminEmail) && string.IsNullOrWhiteSpace(emailDbo.AdminSubject)
&& string.IsNullOrWhiteSpace(emailDbo.AdminBody) && !emailDbo.AdminDateSent.HasValue)
{
return null;
}
return new Email(emailDbo.SendTo, emailDbo.Subject, emailDbo.Body, emailDbo.DateSent);
}
}
Here is the complete fiddle - https://dotnetfiddle.net/2MxSdt

AutoMapper, mapping one type in two different types

I have the following models:
public class Stuff
{
...
public IList<Place> Places { get; set; } = null!;
...
}
public class Place
{
...
public IList<Stuff> Stuffs { get; set; } = null!;
...
}
public class StuffEntity
{
...
public IList<PlaceStuffEntity> Places { get; set; } = null!;
...
}
public class PlaceEntity
{
...
public IList<PlaceStuffEntity> Stuffs { get; set; } = null!;
...
}
public class PlaceStuffEntity
{
public int StuffId { get; private set; }
public StuffEntity Stuff { get; set; } = null!;
public int PlaceId { get; private set; }
public PlaceEntity Place { get; set; } = null!;
}
cfg.CreateMap<StuffEntity, Stuff>()
.ForMember(d => d.Places,
opt => opt.MapFrom(s => s.Places.Select(y => y.Place).ToList()));
cfg.CreateMap<PlaceEntity, Place>()
.ForMember(d => d.Stuffs,
opt => opt.MapFrom(s => s.Places.Select(y => y.Stuff).ToList()));
cfg.CreateMap<PlaceAndStuffEntity, Stuff>() // < -- Issue
.IncludeMembers(entity=> entity.Stuff);
cfg.CreateMap<PlaceAndStuffEntity, Place>() // < -- Issue
.IncludeMembers(entity=> entity.Place);
by some reason when I add both last lines, conversion does not work ...
But if I add only one line for example for converting PlaceAndStuffEntity -> Stuff works only one conversion from PlaceEntity -> Place
var place = mapper.Map<Place>(placeEntity); // <- This works
var stuff = mapper.Map<Stuff>(stuffEntity); // <- Does not work !!
Is there a way properly handle the following conversions ?
It sounds like you want to map through the joining table (PlaceAndStuff) to get to the other entity type. For instance in your Place to get a list of Stuff, and Stuff to get a list of Place, you want to direct Automapper how to navigate through the joining table.
For instance:
cfg.CreateMap<StuffEntity, Stuff>()
.ForMember(x => x.Places, opt => opt.MapFrom(src => src.PlaceEntity));
// Where StuffEntity.Places = PlaceAndStuffEntities, to map Stuff.Places use PlaceAndStuffs.PlaceEntity
cfg.CreateMap<PlaceEntity, Place>()
.ForMember(x => x.Stuffs, opt => opt.MapFrom(src => src.StuffEntity));
So rather than trying to tell EF how to map the joining entity PlaceStuffEntity, we focus on the PlaceEntity and StuffEntity, and tell Automapper to navigate through the joining entity to get at the actual Stuff and Place relatives via the joining entity.
Change
cfg.CreateMap<PlaceEntity, Place>()
.ForMember(d => d.Stuffs,
opt => opt.MapFrom(s => s.Places.Select(y => y.Stuff).ToList()));
to
cfg.CreateMap<PlaceEntity, Place>()
.ForMember(d => d.Stuffs,
opt => opt.MapFrom(s => s.Stuffs.Select(y => y.Stuff).ToList()));
Source type PlaceEntity does not have a property named Places, only Stuffs.

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 :)

resolving complex entity model to flat model view using AutoMapper

I want to create a map for a somewhat complex entity model to a flattened view Model
My entity model is like so
cbItems
has many cbItemsContent
has many cbRegulators
so my viewmodels are like so
for cbItems:
public class ItemViewModel
{
public ItemViewModel()
{
this.CbItemsContents = new HashSet<ItemContentViewModel>();
}
public int ItemID { get; set; }
......
public virtual ICollection<ItemContentViewModel> CbItemsContents { get; set; }
}
}
for cbItemsContent:
public class ItemContentViewModel
{
public int ItemContentID { get; set; }
public int ItemID { get; set; }
....
public ItemContentRegulatorsViewModel RegulatedBy { get; set; }
}
}
for cbRegulators:
public class ItemContentRegulatorsViewModel
{
public int ItemContentId { get; set; }
public IEnumerable<int> RegulatorIds { get; set; }
}
}
I had hoped it would be as easy as this:
config.CreateMap<CbItem, ItemViewModel>();
config.CreateMap<CbItemsContent, ItemContentViewModel>()
.ForMember(dest => dest.RegulatedBy.ItemContentId,
m => m.MapFrom(src => src.GenericID))
.ForMember(dest => dest.RegulatedBy.RegulatorIds,
n => n.MapFrom(src => src.cbItemsContentRegulators.Select(q => q.cbRegulator.RegulatorId)));
from teh following query:
ItemViewModel item =
_context.cbItems.Where(u => u.ItemId = id)
.ProjectTo<ItemViewModel>()
.first();
But this results in an error:
Expression 'dest => dest.RegulatedBy.ItemContentId' must resolve to
top-level member and not any child object's properties. Use a custom
resolver on the child type or the AfterMap option instead. Parameter
name: lambdaExpression
HOw can I achieve my desired model layout?
You have to map ItemContentRegulatorsViewModel, then you don't need to set it from the ViewModel above.
#Rabban probably means something like this:
config.CreateMap<CbItemsContent, ItemContentViewModel>()
.ForMember(dest => dest.RegulatedBy, o => o.MapFrom(src => src));
config.CreateMap<CbItemsContent, ItemContentRegulatorsViewModel>()
.ForMember(dest => dest.ItemContentId, o => o.MapFrom(src => src.GenericID))
.ForMember(dest => dest.RegulatorIds, o => o.MapFrom(src => src.cbItemsContentRegulators.Select(q => q.cbRegulator.RegulatorId)));

How to automap this(mapping sub members)

I have something like this
public class ProductViewModel
{
public int SelectedProductId { get; set; }
public string ProductName {get; set;}
public int Qty {get; set;}
public List<SelectListItem> Products { get; set};
}
I have a domain like this
public class Product
{
public int ProductId {get; set;}
public string ProductName {get; set;}
public int Qty {get; set;}
}
public class Store
{
public Product() {get; set;}
}
Now I need to do the mapping.
// in my controller
var result = Mapper.Map<ProductViewModel, Store>(Product);
this won't bind anything since it can't figure out how to put the ProductId in since it
is
Store.Product.ProductId;
My map is like this
Mapper.CreateMap<ProductViewModel, Store>().ForMember(dest => dest.Product.ProductId, opt => opt.MapFrom(src => src.SelectedProductId));
I get this error
Expression 'dest =>
Convert(dest.Product.SelectedProductId' must
resolve to top-level member. Parameter
name: lambdaExpression
I am unsure how to do this.
To Map nested structures, you just need to create a new object in the MapFrom argument.
Example
Mapping:
Mapper.CreateMap<Source, Destination>()
.ForMember(d => d.MyNestedType, o => o.MapFrom(t => new NestedType { Id = t.Id }));
Mapper.AssertConfigurationIsValid();
Test Code:
var source = new Source { Id = 5 };
var destination = Mapper.Map<Source, Destination>(source);
Classes:
public class Source
{
public int Id { get; set; }
}
public class Destination
{
public NestedType MyNestedType { get; set; }
}
public class NestedType
{
public int Id { get; set; }
}
You can use Resolver.
Create a resolver class like that :
class StoreResolver : ValueResolver<Store, int>
{
protected override int ResolveCore(Store store)
{
return store.Product.ProductId;
}
}
And use it like that :
Mapper.CreateMap<ProductViewModel, Store>()
.ForMember(dest => dest.SelectedProductId, opt => opt.ResolveUsing<StoreResolver >());
Hope it will help ...
The error your getting is because you cannot declare mapping declarations more than one level deep in your object graph.
Because you've only posted one property its hard for me to give you the codes that will make this work. One option is to change your viewmodel property to MyTestTestId and the conventions will automatically pick up on that.
The correct answer given by allrameest on this question should help: AutoMapper - Deep level mapping
This is what you need:
Mapper.CreateMap<ProductViewModel, Store>()
.ForMember(dest => dest.Product, opt => opt.MapFrom(src => src));
Mapper.CreateMap<ProductviewModel, Product>()
.ForMember(dest => dest.ProductId, opt => opt.MapFrom(src => src.SelectedProductId));
NOTE: You should try to move away from using Mapper.CreateMap at this point, it is obsolete and will be unsupported soon.

Categories