resolving complex entity model to flat model view using AutoMapper - c#

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

Related

Default mapping configuration for inherited classes in AutoMapper

Is it possible to create a default destination mapping in AutoMapper ?
Source classes:
class SourceA {
public string X { get; set; }
}
class SourceB {
public string Y { get; set; }
}
Destination classes:
class DestBase {
public List<string> Z { get; set; }
}
class DestA : DestBase {
public string X { get; set; }
}
class DestB : DestBase {
public string Y { get; set; }
}
And the mapping configuration contains the following:
cfg.CreateMap<SourceA, DestA>()
.ForMember(dest => dest.Z, src => src.MapFrom(s => null));
cfg.CreateMap<SourceB, DestB>()
.ForMember(dest => dest.Z, src => src.MapFrom(s => null));
Is it possible to create a default mapping for all destination classes inheriting the DestBase to avoid the repeated .ForMember(...) lines ?
eg. something like:
cfg.CreateMap<object, DestBase>
.ForMember(dest => dest.Z, src => src.MapFrom(s => new List<string>()));
In principle yes, with the Include method, but there is a caveat.
If you define a map from source type object, this map would match all types. Maybe you can introduce an interface ISource for the source types that should be affected by this mapping.
So it could look like this:
class SourceA : ISource {
public string X { get; set; }
}
class SourceB : ISource {
public string Y { get; set; }
}
cfg.CreateMap<ISource, DestBase>
.Include<SourceA, DestA>
.Include<SourceB, DestB>
.Include<SourceC, DestC>
.ForMember(dest => dest.Z, , o => o.MapFrom(src => new List<string>()));
cfg.CreateMap<SourceA, DestA>()
.ForMember(dest => dest.X, o => o.MapFrom(src => src.X));
cfg.CreateMap<SourceB, DestB>()
.ForMember(dest => dest.Y, o => o.MapFrom(src => src.Y));
// still need to create a map even if no additional properties are to be mapped
cfg.CreateMap<SourceC, DestC>();
Note that you still need to create maps for all included types, even if there are no additional properties to map.

Map List of Customers with automapper

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

My loaded children are being lost when using automapper

I am mapping my entity object with Automapper and as you will see in the comments below my Collection of groups gets lost after mapping.
public partial class WebUser
{
public WebUser()
{
this.WebUserGroups = new HashSet<WebUserGroup>();
}
public int UserKey { get; set; }
//stuff
public virtual ICollection<WebUserGroup> WebUserGroups { get; set; }
}
public void Edit(WebUser user)
{
//Has collection of WebUserGroups populated
var userToBeEdited = GetUserBy(user.UserKey);
var updatedWebUser = Mapper.Map(user, userToBeEdited);
//Lost after mapping
_context.Entry(updatedWebUser).State = EntityState.Modified;
_context.SaveChanges();
}
public WebUser GetUserBy(int userKey)
{
return (from webUser in _context.WebUsers
join webUserGroup in _context.WebUserGroups on webUser.UserKey equals webUserGroup.UserKey
select webUser).FirstOrDefault();
}
Mapper.CreateMap<WebUser, WebUser>()
.ForSourceMember(dest => dest.UserKey, src => src.Ignore())
.ForSourceMember(dest => dest.WebUserGroups, src => src.Ignore());
Any Suggestions?
I think you want .ForMember instead of .ForSourceMember:
Mapper.CreateMap<WebUser, WebUser>()
.ForMember(dest => dest.UserKey, opt => opt.Ignore())
.ForMember(dest => dest.WebUserGroups, opt => opt.Ignore());

AutoMapper: Mapping Tuple to Tuple

I'm using AutoMapper in my ASP.NET MVC4 project. I have a problem when mapping 2 class Question and QuestionViewModel. Here my two model classes:
public class Question
{
public int Id { get; set; }
public string Content { get; set; }
public Tuple<int, int> GetVoteTuple()
{
"some code here"
}
}
public class QuestionViewModel
{
public int Id { get; set; }
public string Content { get; set; }
public Tuple<int, int> VoteTuple { get; set; }
}
Here is my controller code :
public class QuestionController: Controller
{
public ActionResult Index(int id)
{
Question question = Dal.getQuestion(id);
Mapper.CreateMap<Question, QuestionViewModel>()
.ForMember(p => p.VoteTuple,
m => m.MapFrom(
s => s.GetVoteTuple()
));
QuestionViewModel questionViewModel =
Mapper.Map<Question, QuestionViewModel>(question);
return View(questionViewModel);
}
}
When I run this code the VoteTuple property in QuestionViewModel has null value. How can I map 2 class with Tuple property ?
Thanks.
Mapping from Tuple to Tuple is not possible by default through Automapper, because Tuple doesn't have setter properties (they can only be initialized through the constructor).
You have 2 options:
1) Create a custom resolver for Automapper and then use the .ResolveUsing method in the mapping config: .ForMember(p => p.VoteTuple, m => m.ResolveUsing<CustomTupleResolver>())
2) Map to a properties / a class instead, like this:
public class QuestionViewModel
{
public int Id { get; set; }
public string Content { get; set; }
public int VoteItem1 { get; set; }
public int VoteItem2 { get; set; }
}
And then:
.ForMember(p => p.VoteItem1, m => m.MapFrom(g => g.Item1))
.ForMember(p => p.VoteItem2, m => m.MapFrom(g => g.Item2))
You don't really need to use Tuple in your view model, so I'd recommend the 2nd option.
Edit:
I see that you've updated your code so that GetVoteTuple() is a function, not a property. In that case, you could easily adapt the code like this:
.ForMember(p => p.VoteItem1, m => m.MapFrom(g => g.GetVoteTuple().Item1))
.ForMember(p => p.VoteItem2, m => m.MapFrom(g => g.GetVoteTuple().Item2))
Your CreateMap call is incorrect:
Mapper.CreateMap<Question, QuestionViewModel>()
.ForMember(p => p.VoteTuple,
m => m.MapFrom(
s => s.GetVoteTuple()
//-----------^
));
Try using ResolveUsing instead of MapFrom (and use the generic s argument in your lambda instead of the local variable reference:
Mapper.CreateMap<Question, QuestionViewModel>()
.ForMember(p => p.VoteTuple,
m => m.ResolveUsing(
s => s.GetVoteTuple()
));
MapFrom is used to map properties directly. Since you're wanting to "map" from the result of a function call, ResolveFrom is more appropriate.
Also, you should only call CreateMap once in your application, typically that's done in Application_Start in global.asax
try this :
Mapper.CreateMap<Question, QuestionViewModel>()
.ForMember(p => p.VoteTuple,op=>op.MapFrom(v=>new Tuple<int,int>(v.GetVoteTuple.Item1,v.GetVoteTuple.Item2)));

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