AutoMapper and nested object mappings for encapsulation - c#

I'm not too familiar with automapper, but it's been thrown on me by our Software Architect for this project.
The concept is complete encapsulation of volatility. Here's a poorly made Diagram
API/Presentation Layer |
Request and Response suffixed objects. (ie, ApplicationCreateRequest)
Business Layer |
Home of Domain transfer objects, suffixed DTO. (ie, ApplicationCreateDTO)
Database Layer |
Home of Resource Access Objects and entities suffixed RAO and Entity (ie, ApplicationEntity, ApplicationCreateRAO)
I need to convert ApplicationCreateRequests to ApplicationCreateDTO's, as well as convert the Requests Nested objects to DTO's as well.
For example:
public class ApplicationCreateRequest
{
public string FirstName { get; set; }
public string LastName { get; set; }
public ContactCreateRequest Contact { get; set; }
public DemographicCreateRequest Demographic { get; set; }
public EducationCreateRequest Education { get; set; }
public WorkCreateRequest Work { get; set; }
}
becomes
public class ApplicationCreateDTO
{
public string FirstName { get; set; }
public string LastName { get; set; }
public ContactCreateDTO Contact { get; set; }
public DemographicCreateDTO Demographic { get; set; }
public EducationCreateDTO Education { get; set; }
public WorkCreateDTO Work { get; set; }
}
The DTO's and Requests have for the most part the same properties.
I've only gotten to work with very basic mappings such as:
CreateMap<ObjectOne, ObjectTwo>();

A easy way to map complex model is to declare, and map them from easiest (with native types eg : strings, int,...) to complex ones.
So, you should create simple mappings for ContactCreateRequest to ContactCreateDTO with CreateMap (and others simplest). Then you will have to create something like :
MapFrom allows you to specify the property (a reason beeing if named differently) to map from. It also allows you to specify a result from a pre-defined mapping, you simply tell it the member you want to Map from...
Mapper.CreateMap<ApplicationCreateRequest, ApplicationCreateDTO>()
.ForMember(g => g.FirstName, opt => opt.MapFrom(src => src.FirstName));
.ForMember(g => g.LastName, opt => opt.MapFrom(src => src.LastName));
.ForMember(g => g.Contact, opt => opt.MapFrom(src => Mapper.Map<ContactCreateRequest,ContactCreateDTO>(g.Contact)));
.ForMember(g => g.Demographic, opt => opt.MapFrom(src => Mapper.Map<DemographicCreateRequest,DemographicCreateDTO>(g.Demographic)));
.ForMember(g => g.Education, opt => opt.MapFrom(src => Mapper.Map<EducationCreateRequest,EducationCreateDTO>(g.Education)));
.ForMember(g => g.Work, opt => opt.MapFrom(src => Mapper.Map<WorkCreateRequest,WorkCreateDTO>(g.Work)));
You can build up by using
.ForMember(g => g.Property, opt => opt.Ignore()); // to ignore the mapping of a property
Handy note, define base mapping before complex ones, otherwise you will have trouble!

Related

Mapping one model to another

I have two models, one of which I process and return it to a view and then from the view I send it to a controller. In the controller, I need to send it to a stored procedure but the stored procedure expects a model with different property names.
Here is my model:
public class Operator
{
public int OPERATOR_OBJECTID { get; set; }
public string SETTLEMENT_OBJECTID { get; set; }
public string TECHNOLOGY_OBJECTID { get; set; }
}
and here is the model the stored procedure expects
public class UploadModel
{
public int OPERATOR_OBJECTID { get; set; }
public string SETTLEMENT_CODE { get; set; }
public string TECHNOLOGY_CODE { get; set; }
}
Since I send the properties from Operator, like SETTLEMENT_OBJECTID but it expects SETTLEMENT_CODE it throws an exception. Can I somehow map the properties from one model to another or can I cast one model to another? What would be a good solution here?
As mentioned in the comments, you can use the automapper library and configure as such:
var mapConfig = new MapperConfiguration(
cfg => cfg.CreateMap<Operator, UploadModel>()
.ForMember(dest => dest.SETTLEMENT_CODE, opt => opt.MapFrom(src => src.SETTLEMENT_OBJECTID))
.ForMember(dest => dest.TECHNOLOGY_CODE, opt => opt.MapFrom(src => src.TECHNOLOGY_OBJECTID))
);
Check here the getting started guide: https://docs.automapper.org/en/stable/Getting-started.html
You can also define an explicit operator to be able to cast from one class to the other: https://www.dotnetperls.com/explicit

How to ignore property of property in AutoMapper mapping?

Image a Person and a Group class with a many-to-many relationship. A person has a list of groups and a group has a list of people.
When mapping Person to PersonDTO I have a stack overflow exception because AutoMapper can't handle the Person>Groups>Members>Groups>Members>...
So here's the example code:
public class Person
{
public string Name { get; set; }
public List<Group> Groups { get; set; }
}
public class Group
{
public string Name { get; set; }
public List<Person> Members { get; set; }
}
public class PersonDTO
{
public string Name { get; set; }
public List<GroupDTO> Groups { get; set; }
}
public class GroupDTO
{
public string Name { get; set; }
public List<PersonDTO> Members { get; set; }
}
When I use .ForMember in creating a mapper, the first mapper that gets executed throws a null reference exception.
Here's the code for the mapper:
CreateMap<Person, PersonDTO>()
.ForMember(x => x.Groups.Select(y => y.Members), opt => opt.Ignore())
.ReverseMap();
CreateMap<Group, GroupDTO>()
.ForMember(x => x.Members.Select(y => y.Groups), opt => opt.Ignore())
.ReverseMap();
So what am I missing or doing wrong? When I remove the .ForMember methods, the null reference exception is not thrown anymore.
UPDATE: I really want to emphasize the main point of my question is how to ignore a property of a property. This code is just a rather simple example.
UPDATE 2: This is how I fixed it, big thanks to Lucian-Bargaoanu
CreateMap<Person, PersonDTO>()
.ForMember(x => x.Groups.Select(y => y.Members), opt => opt.Ignore())
.PreserveReferences() // This is the solution!
.ReverseMap();
CreateMap<Group, GroupDTO>()
.ForMember(x => x.Members.Select(y => y.Groups), opt => opt.Ignore())
.PreserveReferences() // This is the solution!
.ReverseMap();
Thanks to .PreserveReferences() the circular references get fixed!
This should just work. See https://github.com/AutoMapper/AutoMapper/wiki/5.0-Upgrade-Guide#circular-references. There is also a PR pending https://github.com/AutoMapper/AutoMapper/pull/2233.
I think the problem you are experiencing comes from wrong assumption that Groups in PersonDTO.Groups are the same as GroupDTO - it cannot be so without the infinite dependency loop. The following code should work for you:
CreateMap<Person, PersonDTO>()
.ForMember(x => x.Groups, opt => opt.Ignore())
.ReverseMap()
.AfterMap((src, dest) =>
{
dest.Groups = src.Groups.Select(g => new GroupDTO { Name = g.Name }).ToList()
});
CreateMap<Group, GroupDTO>()
.ForMember(x => x.Members, opt => opt.Ignore())
.ReverseMap()
.AfterMap((src, dest) =>
{
dest.Members = src.Members.Select(p => new PersonDTO { Name = p.Name }).ToList()
});
You basically need to teach AutoMapper that in case of PersonDTO.Groups property it should map GroupDTO objects differently.
But I think that your problem is more like architectural issue than code one. PersonDTO.Groups should not be of type GroupDTO - you are here only interested in groups particular user belongs to and not other members of his groups. You should have some simpler type like:
public class PersonGroupDTO
{
public string Name { get; set; }
}
(the name is up to you of course) to only identify the group without passing additionally members.

Expand into a DTO's navigation property using AutoMapper (using EF)

I am trying to expose a simple data model using an WebAPI OData service. My data is stored in a database which I am accessing using EF. Because the tables in my DB use different property names and there are too many fields which are needed in my DTO's, I use AutoMapper to map (or rather, ProjectTo) from DB objects to DTO's.
DTO's are as follows:
public class OrderDTO
{
[Key]
public int SalesOrderNumber { get; set; }
//Navigation
public virtual IEnumerable<OrderLineDTO> OrderLines { get; set; }
}
public class OrderLineDTO
{
[Key]
[ForeignKey("Order")]
public int SalesOrderNumber { get; set; }
[Key]
public int LineNumber { get; set; }
//Navigation
public virtual OrderDTO Order { get; set; }
}
When I access a specific order I get my repsonse as expected (ie: the requested order):
http://localhost/.../Orders(salesOrderNumber=1)
When trying to expand the orderLines property using following request I get the below response:
http://localhost/.../Orders(salesOrderNumber=1)?$expand=orderLines
{
"#odata.context": "http://localhost/.../$metadata#Orders/$entity",
"division": "STAND",
"salesOrderNumber": 1,
"orderLines#odata.context": "http://localhost/.../$metadata#Orders(salesOrderNumber=1)/orderLines",
"orderLines": []
}
I suspect the reason my orderLines object is empty is because AutoMapper doesn't take the expand into consideration when converting from DB objects to DTO's. My entity query looks like this:
_dbContext.ORDERS
.Include("LINES")
.Where(o => (o.ORD_NUM == salesOrderNumber))
.ProjectTo<OrderDTO>()
.FirstOrDefault();
AutoMapper has mappings for both OrderDTO and OrderLineDTO. Configured as follows:
CreateMap<ORDERS, OrderDTO>()
.ForMember(dest => dest.SalesOrderNumber, opt => opt.MapFrom(src => src.ORD_NUM));
CreateMap<LINES, OrderLineDTO>()
.ForMember(dest => dest.SalesOrderNumber, opt => opt.MapFrom(src => src.ORD_NUM))
.ForMember(dest => dest.LineNumber, opt => opt.MapFrom(src => src.LIJNNR));
It seems to me that this should be enough to be able to $expand into my orderLines but I can't get this to work. What am I missing here?
It really seems to me you are missing explicit configuration for mapping ORDERS.LINES to OrderDto.OrderLines:
CreateMap<ORDERS, OrderDTO>()
.ForMember(dest => dest.OrderLines, opt => opt.MapFrom(src => src.LINES))
.ForMember(dest => dest.SalesOrderNumber, opt => opt.MapFrom(src => src.ORD_NUM));

Need help with AutoMapper

I have two classes that looks as follows:
public class Rule
{
public int Id { get; set; }
public RuleGroup RuleGroup { get; set; }
}
public class RuleGroup
{
public int Id { get; set; }
public List<Rule> RuleList { get; set; }
}
A RuleGroup has a list of rules. My AutoMapper settings are as follows:
Mapper.CreateMap<RuleRecord, FirstSolar.Mes.Core.Entities.Recipe.Rule>()
.ForMember(destination => destination.RuleGroup, source => source.Ignore())
.ForMember(destination => destination.Id, source => source.MapFrom(item => item.RuleId));
Mapper.CreateMap<IList<RuleRecord>, IList<FirstSolar.Mes.Core.Entities.Recipe.Rule>>();
Mapper.CreateMap<RuleGroupRecord, FirstSolar.Mes.Core.Entities.Recipe.RuleGroup>()
.ForMember(destination => destination.Id, source => source.MapFrom(item => item.RuleGroupId));
Mapper.CreateMap<IList<RuleGroupRecord>, IList<FirstSolar.Mes.Core.Entities.Recipe.RuleGroup>>();
When I attempt to map a RuleGroupRecord (LinqToSQL object) to RuleGroup (DTO), AutoMapper says I need to add a mapping for RuleGroup.RuleList. I'm wondering why because I defined how to map a single RuleRecord and a List.
If I have to, how would I do it?
Simply add (I hope I got the syntax right, but you should see what I'm hinting at):
.ForMember(destination => destination.RuleList, source => source.MapFrom(item => item.Rules));
to the second mapping. While you handled the general mapping for RuleRecord to Rule in the first mapping, you didn't tell automapper to map the specific property RuleGroup.RuleList.

C# automapper nested collections

I have a simple model like this one:
public class Order{
public int Id { get; set; }
... ...
public IList<OrderLine> OrderLines { get; set; }
}
public class OrderLine{
public int Id { get; set; }
public Order ParentOrder { get; set; }
... ...
}
What I do with Automapper is this:
Mapper.CreateMap<Order, OrderDto>();
Mapper.CreateMap<OrderLine, OrderLineDto>();
Mapper.AssertConfigurationIsValid();
It throw an exception that says:
"The property OrderLineDtos in OrderDto is not mapped, add custom mapping ..."
As we use a custom syntax in our Domain and in our DomainDto, how I can specify that the collection OrderLineDtos in OrderDto corresponds to OrderLines in Order?
Thank you
It works in this way:
Mapper.CreateMap<Order, OrderDto>()
.ForMember(dest => dest.OrderLineDtos, opt => opt.MapFrom(src => src.OrderLines));
Mapper.CreateMap<OrderLine, OrderLineDto>()
.ForMember(dest => dest.ParentOrderDto, opt => opt.MapFrom(src => src.ParentOrder));
Mapper.AssertConfigurationIsValid();
Nested collections work, as long as the names match up. In your DTOs, you have the name of your collection as "OrderLineDtos", but in the Order object, it's just "OrderLines". If you remove the "Dtos" part of the OrderLineDtos and ParentOrderDto property names, it should all match up.

Categories

Resources