I am having problems mapping my domain object to my DTO object.
The error is:
Expression must resolve to top-level member and not any child object's properties. You can use ForPath, a custom resolver on the child type or the AfterMap option instead.
public class Sign
{
public List<Item> Items { get; set; } = new List<Item>();
}
public class SignDTO
{
public SignItemDTO Items { get; set; } = new SignItemDTO();
}
public class SignItemDTO
{
public List<ItemDTO> Items { get; set; } = new List<ItemDTO>();
}
private MapperConfiguration AutoMapperConfig()
{
return new MapperConfiguration(cfg =>
{
cfg.CreateMap<Item, ItemDTO>();
cfg.CreateMap<Sign, SignDTO>().ForPath(dest => dest.Items.Items, opt => opt.MapFrom(src => src.Items));
});
}
_context.Sign.Include(m => m.Items)
.ProjectTo<SignDTO>(AutoMapperConfig());
Probably it's due to your map in this line:
cfg.CreateMap<Sign, SignDTO>().ForPath(dest => dest.**Items.Items**, opt => opt.MapFrom(src => src.**Items**));
As you can see in between the **, you're maping from an entity property to another entity children's property, this is what AutoMapper does not like and what you see in the error message.
Maybe it was a mistake on your side an you can removed one of the Items or you can create a new mapping for your first level of Items which takes care of the childrens.
Related
How to map following ItemId (as Source) with DataId (as Output):
Source:
public class Source
{
public InputData InputItem { get; set; }
}
public class InputData
{
public int ItemID { get; set; }
}
Output:
public class Output
{
public List<OutputData> OutputItem { get; set; }
}
public class OutputData
{
public string[] DataID { get; set; }
}
I'm trying to map it in following way:
CreateMap<Source, Output>().ForMember(d => d.OutputItem[0].DataID,
option => option.MapFrom(s => s.InputItem != null ? new string[] { $"item_{s.InputItem.ItemID}" } : null));
Getting the exception:
Expression 'd => d.OutputItem.get_Item(0).DataID' 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
Can someone please help me in mapping these objects.
Thanks
The error basically says that you can only define a mapping for one level of the object. You can not define the mapping to directly write to a property of a child object.
Solve this by defining separate mappings for the objects on each level.
First define the mapping for the child objects:
CreateMap<InputData, OutputData>().ForMember(d => d.DataID,
option => option.MapFrom(s => s != null ? new string[] { $"item_{s.ItemID}" } : null));
Then the mapping for the parent object can use this definition to map its child objects. (Collections like List<OutputData> should be filled by AutoMapper automatically.)
CreateMap<Source, Output>().ForMember(d => d.OutputItem,
option => option.MapFrom(s => s.InputItem));
I have a model which has a generic properties property looking something like this:
public class GenericProperty
{
public string Name { get; set; }
public object Value { get; set; }
}
Next to that I have a object that has a list with GenericProperties like this:
public class GenericEntity
{
public string Name { get; set; }
public List<GenericProperty> Properties { get; set; }
}
Now im calling a API that deserialize the json to the model above. Next i want to use AutoMapper to construct an actual good looking model so i did the following:
Mapper.Initialize(x =>
{
x.CreateMap<GenericEntity, MyModel>()
.ForMember(d => d.ManagerId, o => o.MapFrom(s => s.Properties.Where(n => n.Name == "ManagerId" ).Select(v => v.Value)))
});
Problem is that this returns the property type and not the actual value:
System.Linq.Enumerable+WhereListIterator`1[MyProject.Models.GenericProperty]
How can i lookup the value of within the model?
By using Where you are selecting all the properties with the name ManagerId. You should instead use Single like in this unit test:
public class TestClass
{
[Test]
public void TestMapper()
{
Mapper.Initialize(x =>
{
x.CreateMap<GenericEntity, MyModel>()
.ForMember(d => d.ManagerId,
o => o.MapFrom(s => s.Properties.Single(n => n.Name == "ManagerId").Value));
});
var ge = new GenericEntity
{
Properties = new List<GenericProperty>
{
new GenericProperty {Name = "ManagerId", Value = "Great"}
}
};
var myModel = Mapper.Map<MyModel>(ge);
Assert.AreEqual("Great", myModel.ManagerId);
}
}
If you can not guarantee that there will be a property with the name ManagerId, you should use SingleOrDefault, and handle the null case.
IMHO, you should limit this kind of AutoMapper configurations to simple cases, since it quickly gets difficult to debug and maintain.
For more complex mappings like this one, I would recommend you to make an explicit mapping method instead, which you can call like an extension method. Another option worth considering if you want to use Automapper are Custom value resolvers.
The Id property in my ViewModel class can be set, but not gotten as the setter encrypts the value. To get the value I have to use a either GetEncryptedId() or GetDecryptedId().
The ViewModel
public ViewModel
{
public int _id;
public int Id { set { _id = Encrypt(value); }
public Child ChildProperty {get; set; }
}
public Child
{
public int Id {get; set;}
public string Name {get; set; }
}
The problem here is that for some reason Auto Mapper wants a get accessor so it can set a value.
public ViewModelProfile()
{
CreateMap<Model, ViewModel>().ForMember(vm => vm.Id, opt => opt.MapFrom(m => m.ChildProperty.Id))
}
This throws and error stating:
CS0154 The property or indexer 'Id' cannot be used in this context because it lacks the get accessor.
Why does Auto Mapper require a 'Get Accessor' to set a value.
Is there a configuration option to force a set and no get?
As a work around I've added a Get { return -1; }, but that is far from ideal.
This isn't a limitation of AutoMapper as such, but of the way its API is structured. The expression tree vm => vm.Id can't be constructed because this is, you've guessed it, a reference to the property getter, which isn't possible because there is no getter. Passing an expression tree as a strongly typed way of referencing a member is a standard technique; AutoMapper is by far not the only thing that would have problems with this. The fact that AutoMapper would not ultimately call the getter is immaterial in this case.
Fortunately, ForMember has an override that accepts the member name as a string, and using that avoids the problem with the expression tree:
CreateMap<Model, ViewModel>()
.ForMember(nameof(ViewModel.Id), opt => opt.MapFrom(m => m.ChildProperty.Id))
This is how i did it:
//Mapper Class
public partial class AutoMapperConfig
{
public static MapperConfiguration UserLoginTovmLogin = null;
public static void Mappings()
{
UserLoginTovmLogin = new MapperConfiguration(cfg =>
cfg.CreateMap<User_Login, VmLogin>()
.ForMember(conf => conf.LoginId, dto => dto.MapFrom(src => src.Login_Id))
.ForMember(conf => conf.Passsword, dto => dto.MapFrom(src => src.Passsword)));
}
}
//In your Data login class
VmLogin IAuthRepository.Login(string loginId)
{
VmLogin login = new VmLogin();
var result = //Your Data Logic
AutoMapperConfig.UserLoginTovmLogin.CreateMapper().Map(result, login);
return login;
}
AutoMapper 5.3.0 alpha (Yet to upgrade to the latest and greatest)
I have a DTO as follows:
public class AoACrudFieldValuesGdataHealthInput
{
public AoACrudFieldValuesGdataHealthInput()
{
PrevHealthAssessment = new HashSet<HealthPrevHealthAssessmentEnum>();
}
public HashSet<HealthPrevHealthAssessmentEnum> PrevHealthAssessment { get; set; }
}
and a EF POCO of:
[Table("Health", Schema = "gdata")]
public class GdataHealthTableModel : AuditedEntity
{
public GdataHealthTableModel()
{
//PrevHealthAssessment = new HashSet<HealthPrevHealthAssessmentEnum>();
}
[ForeignKey("Id")]
public ICollection<GdataHealthPrevAssesmentTableModel> PrevHealthAssessment { get; set; }
}
and:
[Table("HealthPrevAssesment", Schema = "gdata")]
public class GdataHealthPrevAssesmentTableModel : AuditedEntity
{
public HealthPrevHealthAssessmentEnum Assessment { get; set; }
}
I need help extending my map:
CreateMap<AoACrudFieldValuesGdataHealthInput, GdataHealthTableModel>();
such that AoACrudFieldValuesGdataHealthInput.PrevHealthAssessment ends up in GdataHealthTableModel.PrevHealthAssessment. Currently I get an error as my CreateMap() is not detailed enough - naturally.
Is this possible? Or do I get the mapper to ignore that field and do it by hand?
NOTE: For both EF POCO's I have omitted the Id field which is an auto increment for sake of brevity.
Define an additional map for HealthPrevHealthAssessmentEnum -> GdataHealthPrevAssesmentTableModel:
CreateMap<AoACrudFieldValuesGdataHealthInput, GdataHealthTableModel>()
.ForMember(dest => dest.PrevHealthAssessment, o => o.MapFrom(src => src.PrevHealthAssessment));
CreateMap<HealthPrevHealthAssessmentEnum, GdataHealthPrevAssesmentTableModel>()
.ForMember(dest => dest.Id, o => o.Ignore()) // auto ID
.ForMember(dest => dest.Assessment , o => o.MapFrom(src => src));
Not sure of AutoMapper can convert an ICollection to a HashSet out of the box, I think it can.
I use Automapper to map from EF entities to view models.
I now have this entity
public class MenuGroup : IEntity
{
public int MenuGroupId { get; set; }
protected ICollection<MenuGroupItem> _menuGroupItems { get; set; }
public IEnumerable<MenuGroupItem> MenuGroupItems { get { return _menuGroupItems; } }
public void AddMenuItem(MenuGroupItem menuGroupItem)
{
_menuGroupItems.Add(menuGroupItem);
}
}
That is an encapsulated collection, I followed instructions here to make this work: http://lostechies.com/jimmybogard/2014/05/09/missing-ef-feature-workarounds-encapsulated-collections/
So I configure it like so this.HasMany(x => x.MenuGroupItems).WithRequired(x => x.BelongsTo).WillCascadeOnDelete(true);
Now the problem I get is when I try to use automapper to map my MenuGroup into a viewmodel.
I run this code: menuGroup = _context.MenuGroups.Project().To<MenuGroupEditModel>().Single(x => x.UniqueUrlFriendlyName == request.UniqueUrlFriendlyName);
and get this error: The specified type member 'MenuGroupItems' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.
Now I can work with the collection, it saves correctly to the database and all is well there it's only when i want to user automapper here that it fails.
If I replace the protected ICollection and public IEnumerable with simply: public ICollection<MenuGroupItem> MenuGroupItems { get; set; } it works right away so the problem lies in automapping with my encapsulated collection.
Update: I also tried this menuGroup = _context.MenuGroups.Include(x => x.MenuGroupItems).Where(x => x.UniqueUrlFriendlyName == request.UniqueUrlFriendlyName).Project().ToSingleOrDefault<MenuGroupEditModel>(); with no difference other than that it errored in the ToSingleOrDefault instead.
Your problem is that Automapper can't modify MenuGroupItems because there is no public setter.
Your solution is changing it to this:
public IEnumerable<MenuGroupItem> MenuGroupItems { get; set; }
public void AddMenuItem(MenuGroupItem menuGroupItem)
{
MenuGroupItems.Add(menuGroupItem);
}
After some more debugging I figured out the Config file looking like this
public MenuGroupConfiguration()
{
this.HasMany(x => x.MenuGroupAssigments).WithRequired(x => x.BelongTo).WillCascadeOnDelete(true);
this.HasMany(x => x.MenuGroupItems).WithRequired(x => x.BelongsTo).WillCascadeOnDelete(true);
}
had not been included leading to that error that now makes sense.
I can add as a general tip that if you don't use auto-mapper for a query but still use your encapsulated collection remember that you have to call decompile for it to work.
like so
var menuGroupsWithType =
_context.MenuGroups.Include(x => x.MenuGroupItems).Include(x => x.MenuGroupAssigments).Where(x => x.MenuGroupAssigments.Any(y => y.AssignToAll == selectedStructureType))
.OrderBy(x => x.Name).Decompile().ToList();