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

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

Related

AutoMapper and nested object mappings for encapsulation

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!

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.

AutoMapper nested mapping with DI

Mapping of nested objects is pretty straightforward with AutoMapper as long as there is a Map definition for the nested objects as well. I am facing an issue with implementing a Map between 2 objects, one of which does not and cannot have a parameter-less constructor. Let's say the Order object is such as below. The services are injected by autofac constructor dependency injection.
public class Order
{
private readonly IOrderDetailsService _orderDetailsService;
public Order(IOrderDetailsService orderDetailsService)
{
_orderDetailsService = orderDetailsService;
}
public string Name { get; set; }
public int Id { get; set; }
[NonSerialized] private IEnumerable<OrderDetails> _details;
public IEnumerable<OrderDetails> Details
{
get
{
_details = _orderDetailsService.GetDetailsByOrderId(Id);
return _details;
}
set { _details = value; }
}
}
public class OrderDetails
{
private readonly IOrderService _orderService;
public OrderDetails(IOrderService orderService)
{
_orderService = orderService;
}
public int OrderId { get; set; }
public int DetailId { get; set; }
public string DetailInfo { get; set; }
public Order Order
{
get { return _orderService.GetOrderById(OrderId); }
}
}
And if there was a View Model such as below:
public class OrderViewModel
{
public string OrderName { get; set; }
public int OrderId { get; set; }
public IEnumerable<OrderDetailsViewModel> Details { get; set; }
}
public class OrderDetailsViewModel
{
public int DetailId { get; set; }
public string DetailInformation { get; set; }
}
How would we go about Mapping Order to OrderViewModel and vice versa?
Order to OrderViewModel and OrderDetails to OrderDetailsViewModel should be straightforward:
Mapper.CreateMap<Order, OrderViewModel>()
.ForMember(dest => dest.OrderName, opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.OrderId, opt => opt.MapFrom(src => src.Id));
Mapper.CreateMap<OrderDetails, OrderDetailsViewModel>()
.ForMember(dest => dest.DetailInformation, opt => opt.MapFrom(src => src.DetailInfo))
The issue is mapping back to Order. Before attempting to do this, I would read Jimmy Bogard's article on two way mapping. Basically AutoMapper was created to map to DTOs from domain objects, not the other way around.
Nevertheless, if you want to map back to Order and OrderDetails, you could use AutoMapper's ability to construct types using an IoC container. This involves registering the container with AutoMapper so that it knows how to resolve types.
As Jimmy points out in the comments, you'll also need to register Order and OrderDetails. I'm not all that familiar with Autofac, but I had success with this:
var builder = new ContainerBuilder();
builder.RegisterType<OrderDetailsService>().As<IOrderDetailsService>();
builder.RegisterType<OrderService>().As<IOrderService>();
/* Register Order and OrderDetails to use themselves: */
builder.RegisterType<Order>().AsSelf();
builder.RegisterType<OrderDetails>().AsSelf();
var container = builder.Build();
/* Register the container with AutoMapper */
Mapper.Configuration.ConstructServicesUsing(container.Resolve);
Now all you need to do is use .ReverseMap and .ConstructUsingServiceLocator to let AutoMapper know to use the IoC container to create the Order and OrderDetails objects:
Mapper.CreateMap<Order, OrderViewModel>()
.ForMember(dest => dest.OrderName, opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.OrderId, opt => opt.MapFrom(src => src.Id))
.ReverseMap()
.ConstructUsingServiceLocator()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.OrderId))
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.OrderName))
.ForMember(dest => dest.Details, opt => opt.Ignore());
Mapper.CreateMap<OrderDetails, OrderDetailsViewModel>()
.ForMember(dest => dest.DetailInformation, opt => opt.MapFrom(src => src.DetailInfo))
.ReverseMap()
.ConstructUsingServiceLocator()
.ForSourceMember(dest => dest.DetailInformation, opt => opt.Ignore())
.ForMember(dest => dest.DetailInfo, opt => opt.Ignore());
I think this is a good option. Thanks to Jimmy Bogard in the comments for pointing me in the right direction.

Mapping DTO back to model with AutoMapper

I have a Fixture model :
public partial class Fixture
{
public int FixtureId { get; set; }
public string Season { get; set; }
public byte Week { get; set; }
//foreign key
public int AwayTeamId { get; set; }
//navigation properties
public virtual Team AwayTeam { get; set; }
//foreign key
public int HomeTeamId { get; set; }
//navigation properties
public virtual Team HomeTeam { get; set; }
public byte? AwayTeamScore { get; set; }
public byte? HomeTeamScore { get; set; }
}
And a Fixture DTO :
public class FixtureDTO
{
public int Id { get; set; }
public string Season { get; set; }
public byte Week { get; set; }
public string AwayTeamName { get; set; }
public string HomeTeamName { get; set; }
public byte? AwayTeamScore { get; set; }
public byte? HomeTeamScore { get; set; }
}
I am using AutoMapper for the mapping and this is my first attempt using it. Here is my mapping :
CreateMap<Fixture, FixtureDTO>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.FixtureId))
.ForMember(dest => dest.AwayTeamName, opt => opt.MapFrom(src => src.AwayTeam.TeamName))
.ForMember(dest => dest.HomeTeamName, opt => opt.MapFrom(src => src.HomeTeam.TeamName));
CreateMap<FixtureDTO, Fixture>();
It works fine in taking the Fixture and mapping it to the FixtureDTO which I use to display the data. But when I want to update the data and pass the FixtureDTO back to map it back to Fixture I get an error.
public HttpResponseMessage PutFixture(int id, FixtureDTO fixture)
{
if (ModelState.IsValid && id == fixture.Id)
{
//do mapping manually here?
var updated = _repository.UpdateFixture(Mapper.Map<Fixture>(fixture));
return Request.CreateResponse(updated ? HttpStatusCode.OK : HttpStatusCode.NotFound);
}
return Request.CreateResponse(HttpStatusCode.BadRequest);
}
This is the error I get :
Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. Refresh ObjectStateManager entries.
Can anybody help with this?
EDIT : Reverse mapping :
CreateMap<Fixture, FixtureDTO>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.FixtureId))
.ForMember(dest => dest.AwayTeamName, opt => opt.MapFrom(src => src.AwayTeam.TeamName))
.ForMember(dest => dest.HomeTeamName, opt => opt.MapFrom(src => src.HomeTeam.TeamName));
CreateMap<FixtureDTO, Fixture>()
.ForMember(dest => dest.FixtureId, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.AwayTeam.TeamName, opt => opt.MapFrom(src => src.AwayTeamName))
.ForMember(dest => dest.HomeTeam.TeamName, opt => opt.MapFrom(src => src.HomeTeamName));
Extension to #lazyberezovsky's answer:
This is an (untested) example of the reverse mapping you might require:
CreateMap<FixtureDTO, Fixture>()
.ForMember(dest => dest.FixtureId,
opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.AwayTeam,
opt => opt.MapFrom(src => new Team
{
TeamName = src.AwayTeamName
}))
.ForMember(dest => dest.HomeTeam,
opt => opt.MapFrom(src => new Team
{
TeamName = src.HomeTeamName
}));
I've used this format in the past and its worked fine, but alternatively you could also create specific mappings for team. Eg:
CreateMap<string, Team>()....
Also, I presume you are doing it, but assert that your configuration is valid. Eg:
[Test]
public void AutoMapper_Configuration_IsValid()
{
Mapper.Initialize(m => m.AddProfile<MyProfile>());
Mapper.AssertConfigurationIsValid();
}
First, you need to create reverse mapping from DTO to entity (provide custom member mappings, if needed):
Mapper.CreateMap<FixtureDTO, Fixture>();
And second - retrieve, map, and update existing entity
if (ModelState.IsValid && id == fixture.Id)
{
Fixture entity = _repository.FindById(fixture.Id);
Mapper.Map(fixture, entity); // Use this mapping method!
var updated = _repository.UpdateFixture(entity);
// etc
}
Generally that exception is thrown by EF when it detects one of the following:
Opimistic Concurrency Violation: This usually occurs when the entity you are trying to edit was modified else where during the time you loaded, edited and saved it. (see: Entity Framework: "Store update, insert, or delete statement affected an unexpected number of rows (0).")
An Incorrectly set ID: No ID set for Pks or FKs. I've also seen an exception like this when I mistakenly set an Entity's FK to a value and set the associated object to an object with a different ID.
Most likely this exception is being thrown because of some code in your repository. If you post the code in your repository we might get a better idea of what is causing the exception.

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