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.
Related
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));
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.
I have 2 objects that I need to map to each other. They look like
public class Example1
{
CustomType1 Prop { get; set; }
List<CustomType1> List { get; set; }
}
public class Example2
{
Customtype2 Prop { get; set; }
List<Customtype2> List { get; set; }
}
public class CustomType1
{
public string SomeString { get; set; }
}
public class Customtype2
{
public string FirstPartOfSomeString { get; set; }
public string SecondPartOfSomeString { get; set; }
}
I want to make one CustomResolver that maps CustomType1 to CustomType2 and then use that resolver on the list. For example,
Mapper.CreateMap<Example1, Example2>()
.ForMember(d => d.Prop, opt => opt.ResolveUsing(myCustomResolver))
.ForMember(d => d.List, opt => opt.ResolveUsing( /*use myCustomResolver on a list here*/));
I have tried using something like:
Mapper.CreateMap<Example1, Example2>()
.ForMember(d => d.Prop, opt => opt.ResolveUsing(myCustomResolver))
.ForMember(d => d.List, opt => opt.MapFrom(s => s.List.Select(myCustomResolver.Resolve).ToList()));
but I seem to be missing something. Is there a way to do this with AutoMapper?
Have you tried adding a mapping between the custom types instead of using a resolver?
AutoMapper is intelligent enough to re-use mappings for lists...
Mapper.CreateMap<CustomType1, CustomType2>()
.ForMember(x => FirstPartOfSomeString, opts => opts.MapFrom(x => x.SomeString.Substring(5)))
.ForMember(x => SecondPartOfSomeString, opts => opts.MapFrom(x => x.SomeString.Substring(5, 5)));
Mapper.CreateMap<Example1, Example2>();
I've begun implementing this;
Automapper, mapping to a complex object but figured there must be a better way.
So I created this;
Mapper.CreateMap<StoreTransportWindow, CSVWindow>()
.ForMember(dest => dest.DC, opt => opt.ResolveUsing(fa => fa.DC.number))
.ForMember(dest => dest.Type, opt => opt.ResolveUsing(fa => fa.Type))
;
Mapper.CreateMap<Store, CSVStore>()
.ForMember(dest => dest.StoreName, opt => opt.ResolveUsing(fa => fa.name))
.ForMember(dest => dest.StoreNumber, opt => opt.ResolveUsing(fa => fa.number))
;
Now I'd like to use the above mappings in the primary map;
Mapper.CreateMap<Store, CSVLineObject>()
.ForMember( dest => dest.store, opt => opt.ResolveUsing(/* This is where I'd like to use the above Store to CSVStore mapping */))
;
Is this possible?
edit
public class CSVStore
{
public string StoreNumber { get; set; }
public string StoreName { get; set; }
}
public class CSVWindow
{
public string Type { get; set; }
public string DC { get; set; }
public string TPC { get; set; }
public class CSVLineObject
{
public CSVStore store { get; set; }
public List<CSVWindow> storeWindows { get; set; }
As mentioned in the comment, the initial mappings should probably be more like:
Mapper.CreateMap<StoreTransportWindow, CSVWindow>()
.ForMember(dest => dest.DC, opt => opt.MapFrom(src => src.DC.number));
// Mapping for property Type not required
Mapper.CreateMap<Store, CSVStore>()
.ForMember(dest => dest.StoreName, opt => opt.MapFrom(src => src.name))
.ForMember(dest => dest.StoreNumber, opt => opt.MapFrom(src => src.number));
Now say you have the following:
public class Source
{
public Store Store { get; set; }
}
public class Destination
{
public CSVStore Store { get; set; }
}
Then the following mapping will suffice (as you've already defined the nested mapping Store to CSVStore):
Mapper.CreateMap<Source, Destination>();
However if Destination was more like this:
public class Destination
{
public CSVStore CSVStore { get; set; }
}
Then you'll need to explicitly define the properties to be mapped:
Mapper.CreateMap<Source, Destination>()
.ForMember(dest => dest.CSVStore, opt => opt.MapFrom(src => src.Store));
(Note that the mapping from Store to CVStore is applied automatically.)
If for some reason you do need to explicitly define a nested mapping, you can do something like this:
Mapper.CreateMap<Source, Destination>()
.ForMember(dest => dest.destproperty,
opt => opt.MapFrom(
src => Mapper.Map<SrcType, DestType>(src.srcproperty));
I have needed to use that at times, but not very often as the default functionality takes care of it for you automatically.
I can provide more details if required if you can expand on your requirements.
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.