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.
Related
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.
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;
}
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();
Given these two objects
public class UserModel
{
public string Name {get;set;}
public IList<RoleModel> Roles {get;set;}
}
public class UserViewModel
{
public string Name {get;set;}
public IList<RoleViewModel> Roles {get;set;} // notice the ViewModel
}
Is this the most optimal way to do the mapping, or is AutoMapper capable of mapping Roles to Roles on its own?
App Config
Mapper.CreateMap<UserModel, UserViewModel>()
.ForMember(dest => dest.Roles, opt => opt.MapFrom(src => src.Roles));
Mapper.CreateMap<UserViewModel, UserModel>()
.ForMember(dest => dest.Roles, opt => opt.MapFrom(src => src.Roles));
Implementation
_userRepository.Create(Mapper.Map<UserModel>(someUserViewModelWithRolesAttached);
Is this the most optimal way to do the mapping, or is AutoMapper capable of mapping Roles to Roles on its own?
If the property names are identical, you should not have to manually provide a mapping:
Mapper.CreateMap<UserModel, UserViewModel>();
Mapper.CreateMap<UserViewModel, UserModel>();
Just make sure the inner types are mapped as well (RoleViewModel ↔ RoleModel)
What this means, however, is that if you change a source or destination property name, AutoMapper mappings can fail silently and cause hard to track down problems (e.g., if you changed UserModel.Roles to UserModel.RolesCollection without changing UserViewModels.Roles).
AutoMapper provides a Mapper.AssertConfigurationIsValid() method that will check all of your mappings for errors and catch misconfigured mappings. It's useful to have a unit test that runs with the build that validates your mappings for this kind of problem.
You don't need to map the properties. Just make sure that the property names match and there is a mapping defined between them.
Mapper.CreateMap<UserModel, UserViewModel>();
Mapper.CreateMap<UserViewModel, UserModel>();
Mapper.CreateMap<RoleModel, RoleViewModel>();
Mapper.CreateMap<RoleViewModel, RoleModel>();
Or with the cooler way I just found out:
Mapper.CreateMap<UserModel, UserViewModel>().ReverseMap();
Mapper.CreateMap<RoleModel, RoleViewModel>().ReverseMap();
All the other answers, are much better (which I gave an upvote to each).
But what I wanted to post here is a quick playground that you could copy and past right into LinqPad in C# program mode and play your idea's without messing with your actual code.
Another awesome thing about moving all your conversions into a TyperConverter class is that your conversions are now Unit Testable. :)
Here you will notice that the model and viewmodel are almost identical except for one property. But through this process the right property is converted to the correct property in the destination object.
Copy this code into LinqPad and you can run it with the play button after switching to C# Program mode.
void Main()
{
AutoMapper.Mapper.CreateMap<UserModel, UserViewModel>().ConvertUsing(new UserModelToUserViewModelConverter());
AutoMapper.Mapper.AssertConfigurationIsValid();
var userModel = new UserModel
{
DifferentPropertyName = "Batman",
Name = "RockStar",
Roles = new[] {new RoleModel(), new RoleModel() }
};
var userViewModel = AutoMapper.Mapper.Map<UserViewModel>(userModel);
Console.WriteLine(userViewModel.ToString());
}
// Define other methods and classes here
public class UserModel
{
public string Name {get;set;}
public IEnumerable<RoleModel> Roles { get; set; }
public string DifferentPropertyName { get; set; }
}
public class UserViewModel
{
public string Name {get;set;}
public IEnumerable<RoleModel> Roles { get; set; } // notice the ViewModel
public string Thingy { get; set; }
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine(string.Format("Name: {0}", Name));
sb.AppendLine(string.Format("Thingy: {0}", Thingy));
sb.AppendLine(string.Format("Contains #{0} of roles", Roles.Count()));
return sb.ToString();
}
}
public class UserModelToUserViewModelConverter : TypeConverter<UserModel, UserViewModel>
{
protected override UserViewModel ConvertCore(UserModel source)
{
if(source == null)
{
return null;
}
//You can add logic here to deal with nulls, empty strings, empty objects etc
var userViewModel = new UserViewModel
{
Name = source.Name,
Roles = source.Roles,
Thingy = source.DifferentPropertyName
};
return userViewModel;
}
}
public class RoleModel
{
//no content for ease, plus this has it's own mapper in real life
}
Result from the Console.WriteLine(userViewModel.ToString());:
Name: RockStar
Thingy: Batman
Contains #2 of roles
Inside the Startup.cs in the Configure() method:
Mapper.Initialize(config => {
config.CreateMap<UserModel, UserViewModel>().ReverseMap();
// other maps you want to do.
});
Within my domain model for my view I have the following object that is serving as backing fields for my properties
public class ModelProperty<T>// where t:struct
{
public T Value { get; set; }
public string Description { get; set; }
public string LabelName { get; set; }
}
The object in turn is presented as:
public partial class Incident : BaseEntityModel
{
private ModelProperty<string> typeCode = new ModelProperty<string>{Description="1-C", LabelName = "Type Code"};
private ModelProperty<string> typeText = new ModelProperty<string>{Description="1-C", LabelName = "Type Text"};
public ModelProperty<string> TypeCode { get {return typeCode;}}
public ModelProperty<string> TypeText { get {return typeText;}}
}
The business object (my source) is not as complex.
public partial class Incident : ObjectBase
{
public string TypeCode { get; set; }
public string TypeText { get; set; }
}
is it possible to map the values from the source to the target. Using Automapper I have the following mapping setup
//note SrcObj is not an object but a namespace alias since the domain and business objects are of the same name
Mapper.CreateMap<SrcObj.Incident, Incident>()
.ForMember(ui => ui.TypeText.Value,
opt => opt.MapFrom(src => src.TypeText));
But I am getting the exception Expression 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.
I'm new to automapper but in looking at the documentation is the object I am working with too complex (based on the idea that there are really three types here and not two)?
If it is possible to handle this type of mapping how is this done?
Update
Based upon the suggestion from Jimmy I have updated my code as follows:
Mapper.CreateMap<SrcObj.Incident, Incident>();
Mapper.CreateMap<string, ModelProperty<string>>()
.ConvertUsing(src => new ModelProperty<string> { Value = src });
Mapper.AssertConfigurationIsValid();
SrcObj.Incident viewModelDto = md.GenerateMockIncident(); //populate the business object with mock data
uibase = Mapper.Map<SrcObj.Incident, Incident>(viewModelDto);
The code executes and I do not get any exceptions however the value that is being set and returned in the business object is still not getting assigned to the backing property Value it is still null.
What am I missing?
-cheers
An easier way is to create a type converter:
Mapper.CreateMap<string, ModelProperty<string>>()
.ConvertUsing(src => new ModelProperty<string> { Value = src });
Then you'll have this for every time AutoMapper sees string -> ModelProperty. You won't have to do member-specific configuration at all.
Try this.. you need to give a ModelProperty object mapping to the destination TypeText
Mapper.CreateMap<Funky.Incident, Incident>()
.ForMember(ui => ui.TypeText,
opt => opt.MapFrom(src =>
new ModelProperty<string>
{
Value = src.TypeText
}));
Do the same for the TypeCode property mapping so that all fields are mapped.
You need to account for each member mapping, only if their names are different OR if their type names are different. in this case, AutoMapper will have a hard time converting string to a Model object till you give hints.
Try mapping TypeCode as well.. And i don't know the properties of ObjectBase etc. So you need to do check if any manual mapping is needed there as well.