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));
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.
I've browsed documentation on mapping collections and nested mapping and mapping of nested collection, but still can't cope with my case.
I have the following json config file:
{
"startupConfig": {
"noSubscription": {
"calls": [
{
"percentage": 30,
"techPriority": 1,
"timePriority": 2
},
{
"percentage": 30,
"techPriority": 1,
"timePriority": 2
}
]
}
}
}
And here is my code reading from the file:
var config = _mapper.Map<FeedConfiguration>(_configuration
.GetSection("startupConfig").GetChildren()
.FirstOrDefault(cc => cc.Key == "noSubscription")?.GetChildren());
However, the mapping does not work. Here is the code of mapping configuration:
CreateMap<IEnumerable<IConfigurationSection>, CallConfiguration>()
.ForMember(cc => cc.Percentage,
mo => mo.MapFrom(css => css.FirstOrDefault(cs => cs.Key == "percentage").Value))
.ForMember(cc => cc.TechPriority,
mo => mo.MapFrom(css => css.FirstOrDefault(cs => cs.Key == "techPriority").Value))
.ForMember(cc => cc.TimePriority,
mo => mo.MapFrom(css => css.FirstOrDefault(cs => cs.Key == "timePriority").Value));
CreateMap<IEnumerable<IConfigurationSection>, FeedConfiguration>()
.ForMember(fc => fc.CallsConfig,
mo => mo.MapFrom(css => css.FirstOrDefault(cs => cs.Key == "calls").GetChildren()));
Classes that I am mapping to:
namespace FeedService.FeedConfigurations
{
public class FeedConfiguration
{
public ICollection<CallConfiguration> CallsConfig { get; set; }
}
public class CallConfiguration
{
public int Percentage { get; set; }
public int TechPriority { get; set; }
public int TimePriority { get; set; }
}
}
And here is an exception I get:
AutoMapper.AutoMapperConfigurationException:
Unmapped members were found. Review the types and members below.
Add a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type
For no matching constructor, add a no-arg ctor, add optional arguments, or map all of the constructor parameters
=============================================================================================================
AutoMapper created this type map for you, but your types cannot be mapped using the current configuration.
IConfigurationSection -> CallConfiguration (Destination member list)
Microsoft.Extensions.Configuration.IConfigurationSection -> FeedService.FeedConfigurations.CallConfiguration (Destination member list)
Unmapped properties:
Percentage
TechPriority
TimePriority
Would be very thankful for your help!
===Note===
I still need an answer for the question but I posted the new one with better explanation here.
You don't actually need Automepper here. Just use default .net core binding.
rename this one
public ICollection<CallConfiguration> CallsConfig { get; set; }
to Calls
then use something like
var config = _configuration.GetSection("startupConfig:noSubscription").Get<FeedConfiguration>();
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.
here is the scenario:
- I have a list of products.
- each product has a number of parts.
- The parts has two types: MAIN and OPTIONAL.
I want to bring the list of the main parts of a product. What would be the statement?
I am currently using this statement but returns an error:
#Html.DisplayFor(p => p.Products.FirstOrDefault().Parts.Where(i => i.PartType == percobaan2.Models.PartType.Main))
Thanks for you help
UPDATE:
here is the error:
An exception of type 'System.InvalidOperationException' occurred in System.Web.Mvc.dll but was not handled in user code
Additional information: Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.
ADDITIONAL INFORMATION:
this code works though. But I need to add the where filter:
#Html.DisplayFor(p => p.Products.FirstOrDefault().Parts)
UPDATED 2:
Here is my viewmodel class updated with mainparts property:
public class ProductDetail
{
public IEnumerable<Product> Products { get; set; }
public IEnumerable<Part> Parts { get; set; }
public IEnumerable<percobaan2.Models.Version> Versions { get; set; }
public IEnumerable<Category> Categories { get; set; }
public IEnumerable<Producer> Producers { get; set; }
public IEnumerable<Part> MainParts
{
get
{
return Parts.Where(p => p.PartType == PartType.Main);
}
}
}
DisplayFor can't be called on an IEnumerable like that. It needs to be a direct property of a model.
#Html.DisplayFor(p => p.Products.FirstOrDefault().Parts) works because Parts is a property that exists on your Product type.
If you need to be able to filter the parts but still want the HTML helper, define another property like so:
public class Product
{
public IEnumerable<Part> MainParts
{
get
{
return Parts.Where(i => i.PartType == PartType.Main);
}
}
}
Then you can use
#Html.DisplayFor(p => p.Products.FirstOrDefault().MainParts)
If you're using an EF-generated class, just make this a partial class and use it as-is.
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.