I’ve run into some issues while using Automapper on an object tree where there are multiple circular references present, but I can’t say if the behavior is intended/expected when using Automapper, if I’m doing something wrong, or if it is indeed a bug. I don’t think what I am doing is particularly novel, so it would surprise me if the observed behavior is unexpected, but in that case I would like to know why it happens and how it can be avoided.
I’m working on an API in ASP.NET Core 8 and am using Automapper (13.0.1) to map from domain objects to a set of DTOs. I’ll isolate the problem I face by focusing on a representative set of objects rather than showing the whole model.
Say I have three objects, Book
, Author
and Publisher
. A book can be authored by more than one author, so the Book
has a List<Author> Authors
property. Conversely, each author can have authored mulitple books, so the Author
object has a List<Book> Books
property. There exists a many-to-many relationship between these two objects (although I have only defined one author for each book). Each book is probably to be issued only by one publisher, so the Book
object also defines a property called Publisher
(of type Publisher
), while the Publisher
object has a List<Book> Books
property. So there is a one-to-many relationship between Book
and Publisher
.
Using EF core, I can retrieve all the books from the data store, List<Book> books = DbContext.Books.ToList()
and by default the Authors
property will be an empty list and Publisher
property will be null
.
If I ask for either the authors or the publisher to be included, e.g. List<Book> books = DbContext.Books.Include(x=>x.Publisher).ToList()
I will have one circular reference through Books.First().Publisher.Books
, and a similar circular reference if I ask for the authors instead. Using MaxDepth(2)
on the mapping profile between the Book
entity and DTO takes care of that.
If I include both the authors and the publisher in the query, I have two circular references: Books.First().Publisher.Books
and Books.First().Authors.First().Books
. When I use Automapper on this object, I get unexpected behaviour (at least for me):
[
{
"id": 1,
"bookName": "Book A",
"publisherId": 1,
"publisher": {
"publisherName": "Publishing house Ltd",
"books": [
{
"bookName": "Book B",
"publisherId": 1,
"authors": [],
"id": 2
},
{
"bookName": "Book C",
"publisherId": 1,
"authors": [],
"id": 3
}
],
"id": 1
},
"authors": [
{
"firstName": "John",
"lastName": "Doe",
"books": [],
"id": 1
}
]
},
{
"id": 2,
"bookName": "Book B",
"publisherId": 1,
"publisher": {
"name": "Publishing house Ltd",
"books": [
{
"bookName": "Book A",
"publisherId": 1,
"authors": [
{
"firstName": "John",
"lastName": "Doe",
"books": [],
"id": 1
}
],
"id": 1
},
{
"bookName": "Book C",
"publisherId": 1,
"authors": [],
"id": 3
}
],
"id": 1
},
"authors": []
},
{
"id": 3,
"bookName": "Book C",
"publisherId": 1,
"publisher": {
"name": "Publishing house Ltd",
"books": [
{
"bookName": "Book A",
"publisherId": 1,
"authors": [
{
"firstName": "John",
"lastName": "Doe",
"books": [],
"id": 1
}
],
"id": 1
},
{
"bookName": "Book B",
"publisherId": 1,
"authors": [],
"id": 2
}
],
"id": 1
},
"authors": []
}
]
In the list of mapped Book
DTOs, the first book (call it Book A) will have the Publisher property populated, and that property’s Books property will be populated as well, with all books except the first book (Book A). Each book inside this property (Book B… ++) will have an empty array in their Authors property. Book A will also have the Authors property populated.
The second book, Book B, will have no Authors property mapped, just the Publisher property. However, unlike for Book A, where inside the Publisher property each Book (except Book A) were mapped in a similar fashion (e.g. each Book’s Authors property was an empty array), now some Books will have their Authors property mapped, while others have empty arrays in this property.
In addition to the different shape of the mapped DTO objects (some have the Authors property populated, while others do not), this also means that the information about who the actual author of Book B and C are is lost. Book B, in my example, was authored by a Author with Id=2 and Book C by an Author with Id=3. All three books were published by the same Publisher (with Id=1).
One solution that seems to work is to perform two database calls on and ask for one attached property at a time. In that case, the properties (either the Publisher or the Authors) will be correctly mapped on the first level of the resulting tree (the Book DTO) for each book in a list of Book
DTOs. I can then copy the Authors object of one set of Book DTOs onto the other set of books (where the Publisher were already correctly mapped).
This, however, feels a bit wrong.
Please let me know if additional information is needed, or if this can be settled by the information I have provided so far.
Thanks!
EXPECTED BEHAVIOR: I tried including more than one attached property in the same database query. When the result is mapped to a DTO with Automapper, the resulting list of objects are not of the same shape. Some have attached properties on the top level, while other has it deeper down the object tree.