Let's say, in the BookDetails page (BookForDetailsDto) we also show the authors of that book (AuthorForListingDto). And moreover, I want to show this author list together with a little info (just the name and id) on the books (BookForAuthorListingDto) of each author.
I have a simple many-to-many relation consisting of Book, Author and BookAuthor objects.
public class Book {
public int Id { get; set; }
public string Name { get; set; }
public List<BookAuthor> Authors { get; set; }
}
public class Author {
public int Id { get; set; }
public string Name { get; set; }
public List<BookAuthor> Books { get; set; }
}
public class BookAuthor {
public int BookId { get; set; }
public Book Book { get; set; }
public int AuthorId { get; set; }
public Author Author { get; set; }
}
And I have also 3 DTOs (where I am stoping an infinite loop):
public class BookForDetailsDto {
public int Id { get; set; }
public string Name { get; set; }
public List<AuthorForListingDto> Authors { get; set; }
}
public class AuthorForListingDto {
public int Id { get; set; }
public string Name { get; set; }
public List<BookForAuthorListingDto> Books { get; set; }
}
public class BookForAuthorListingDto {
public int Id { get; set; }
public string Name { get; set; }
}
Having a configuration as the following:
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<Book, BookForDetailsDto>();
cfg.CreateMap<BookAuthor, AuthorForListingDto>();
cfg.CreateMap<AuthorForListingDto, BookForAuthorListingDto>();
});
I'd like to perform a mapping from Book to BookForDetailsDto like this.
BookForDetailsDto BookDto = mapper.Map<BookForDetailsDto>(book);
But as a result, I get System.NullReferenceException.
It seems like, just in the first level of mapping, AutoMapper cannot get Author information from BookAuthor object.
I am searching for a configuration option but with no luck. I should say I am a newbie with automapper and if there is a simple solution I appreciate.
Note: I saw a comment which goes like "it is not a good practice to have reference in one DTO to second DTO". But I cannot figure out how to do otherwise, because ,for example, for a clickable/navigatable child_object we need at least "a key and a display_name", so a child object of type List seems inevitable.
A new day with a new head...
I changed the mappings like the following and it works as expected:
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Book, BookForDetailsDto>()
.ForMember(dto => dto.Authorss, opt => opt.MapFrom(x => x.Authors.Select(a => a.Author)));
cfg.CreateMap<BookAuthor, BookForAuthorListingDto >()
.ForMember(res => res.Id, opt => opt.MapFrom(dto => dto.Book.Id))
.ForMember(res => res.Name, opt => opt.MapFrom(dto => dto.Book.Name));
});
Related
I am trying to use AutoMapper to map the Domain model to Dtos in ASP.NET Core Web API.
Domain class
public partial class Category
{
public Category()
{
Products = new HashSet<Product>();
}
public int Id { get; set; }
public int CategoryDiscount { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
Domain Product Class
public partial class Product
{
public int Id { get; set; }
public int CategoryId { get; set; }
public decimal Price { get; set; }
public virtual Category Category { get; set; } = null!;
}
DTO class
public class GetCategoryProductsDto
{
public int Id { get; set; }
public string CategoryName { get; set; } = string.Empty;
public int CategoryDiscount { get; set; }
public List<ProductDto> Products { get; set; }
}
Mapper Configuration
CreateMap<Category, GetCategoryProductsDto>();
Everything works fine however I wanted to calculate the product price after deducting the Category Discount.
Solution 1: With .AfterMap()
To perform the Price calculation after mapping, you can use .AfterMap().
CreateMap<Product, ProductDto>();
CreateMap<Category, GetCategoryProductsDto>()
.AfterMap((src,dest) =>
{
dest.Products.ForEach(x => x.Price -= src.CategoryDiscount);
});
Demo Solution 1 # .NET Fidde
Solution 2: With Custom Value Resolver
As #Lucian suggested, a custom value resolver is another option to handle the Price calculation.
CreateMap<Product, ProductDto>()
.ForMember(
dest => dest.Price,
opt => opt.MapFrom((src, dest, member, context) => src.Price - src.Category.CategoryDiscount));
CreateMap<Category, GetCategoryProductsDto>();
Demo Solution 2 # .NET Fiddle
I'm trying to map a DTO to a Type. I did already did it the other way around.
The tricky part is getting the Many-to-Many relationship right.
Where Book has an ICollection<BookCategory> the BookDTO has an ICollection<int>.
namespace Core.Entities
{
public class Book : IAggregateRoot
{
public int BookId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string CoverImageUrl { get; set; }
public string Layout { get; set; }
public ICollection<Chapter> Chapters { get; set; }
public ICollection<BookCategory> BookCategories { get; set; }
public ICollection<BookTag> BookTags { get; set; }
}
}
namespace API.DTOs
{
public class BookDTO : DTO
{
public int BookId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string CoverImageUrl { get; set; }
public string Layout { get; set; }
public ICollection<int> Categories { get; set; }
public ICollection<int> Tags { get; set; }
}
}
namespace Core.Entities
{
public class BookCategory : IAggregateRoot
{
public int BookId { get; set; }
public Book Book { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
}
}
In my MappingProfile I have this code:
namespace API
{
public class MappingProfile : Profile
{
public MappingProfile()
{
// Entity -> DTO
// src being the concrete type T in CreateMap<T, DTO>()
CreateMap<Book, BookDTO>()
.ForMember(dto => dto.Categories, options => options.MapFrom(src => src.BookCategories.Select(bc => bc.CategoryId).ToList()))
.ForMember(dto => dto.Tags, options => options.MapFrom(src => src.BookTags.Select(bt => bt.TagId).ToList()));
...
// DTO -> Enitity
CreateMap<BookDTO, Book>()
.ForMember(
book => book.BookCategories,
options => options.ConvertUsing(dto => dto.Categories.Select(category => new BookCategory
{
BookId = dto.BookId,
CategoryId = category
}).ToList()));
...
// The simplified version
CreateMap<List<int>, List<BookCategory>>().ConstructUsing(listOfInts => listOfInts.Select(singleInt => new BookCategory {
BookId = singleInt,
CategoryId = singleInt
}).ToList());
}
// An external conversion function
// Usage:
// ConvertUsing(dto => ConvertBookDtoToList(dto))
private ICollection<BookCategory> ConvertBookDtoToList(BookDTO dto)
{
return dto.Categories.Select(category => new BookCategory {
BookId = dto.BookId,
CategoryId = category
}).ToList();
}
}
}
However using any of these approaches results in an error:
Severity Code Description Project File Line Suppression State
Error CS0411 The type arguments for method 'IMemberConfigurationExpression<BookDTO, Book, object>.ConvertUsing<TValueConverter, TSourceMember>(Expression<Func<BookDTO, TSourceMember>>)' cannot be inferred from the usage. Try specifying the type arguments explicitly. API C:\Users\pebes\source\repos\ip6_uiux-ereader-app\book_designer\API\MappingProfile.cs 40 Active
I don't really understand the error message, as would appear to me to make sense.
How can I resolve this?
CovertUsing is using for another purpose and it must be after CreateMap. It`s using to set up another way to convert one value to another
Instead of ConvertUsing you must use MapFrom, which do what you want:
CreateMap<BookDTO, Book>()
.ForMember(
book => book.BookCategories,
options => options.MapFrom(dto =>
dto.Categories.Select(category => new BookCategory
{
BookId = dto.BookId,
CategoryId = category
}).ToList()
)
);
How to show child data in view here I have two models Book (parent) and Author (child).
I want to show author name on view but in Book model I have Author object with null reference, but AuthorId has value 1.
I succeeded to get child values in controller when I got Book data with DbContext object I search for Author data, I don't know is this correct way because I am new with ASP.NET MVC or there exists better solution to connect data between parent and child data?
Here is my controller and model classes.
public ActionResult Book(int id)
{
var book = dbContext.Books.SingleOrDefault(b => b.ID == id);
if(book.AuthorId != null)
{
book.Author = dbContext.Authors.SingleOrDefault(a => a.ID == book.AuthorId);
}
return View(book);
}
Book model class.
public class Book
{
[Key]
public int ID { get; set; }
public string Title { get; set; }
public decimal Score { get; set; }
public Format Format { get; set; }
public Language Language { get; set; }
[ForeignKey("Author")]
public int? AuthorId { get; set; }
public Author Author { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public decimal Discount { get; set; }
}
And this child model Author
public class Author
{
[Key]
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public ICollection<Book> Books { get; set; }
}
EDIT:
Solution is to use Include extension method which can be found in this namespace System.Data.Entity.
public ActionResult Book(int id)
{
var book = dbContext.Books
.Include(a => a.Author)
.Include(p => p.ProductDetails)
.SingleOrDefault(b => b.ID == id);
return View(book);
}
Code does not seem to have any errors. Make sure your Database Table Column names match exactly with Property name. And try different ways of adding forign key in EF.
Adding Foreign Key
I have a situation where I need to map a sub-collection of items within an object to a collection of items in another object. I am essentially trying to flatten the object for use by a consuming system.
Given the following entity classes:
public class PersonEntity
{
public int Id { get; set; }
public virtual ICollection<OutcomeEntity> Outcomes { get; set; }
}
public class OutcomeEntity
{
public int Id { get; set; }
public bool Outcome { get; set; }
public virtual ICollection<GradeEntity> Grades { get; set; }
public PersonEntity Person { get; set; }
}
public class GradeEntity
{
public int Id { get; set; }
public string Grade { get; set; }
public string MarkersComment { get; set; }
public OutcomeEntity Outcome { get; set; }
}
I need to map the OutcomeEntity and GradeEntity to the following flattened structure where there can be many outcomes, containing many different grades:
public class PersonDTO
{
public int PersonId { get; set; }
public virtual ICollection<GradeDTO> Grades { get; set; }
}
public class GradeDTO
{
public int OutcomeId { get; set; }
public int GradeId { get; set; }
public string Grade { get; set; }
public string MarkersComment { get; set; }
}
Basically, for every Outcome in the collection, I want to iterate over the grades within it and create a new object (GradeDTO).
I have attempted to create a basic map, but I simply cannot get my head around the sub-properties.
To create one collection from many you can use SelectMany extension method. With this method and the following configuration AutoMapper will create PersonDto from PersonEntity.
Mapper.Initialize(cfg =>
{
cfg.CreateMap<GradeEntity, GradeDTO>()
.ForMember(dto => dto.GradeId, x => x.MapFrom(g => g.Id))
.ForMember(dto => dto.OutcomeId, x => x.MapFrom(g => g.Outcome.Id));
cfg.CreateMap<PersonEntity, PersonDTO>()
.ForMember(dto => dto.PersonId, x => x.MapFrom(p => p.Id))
.ForMember(dto => dto.Grades, x => x.MapFrom(p => p.Outcomes.SelectMany(o => o.Grades)));
});
Hi I have a list of people that also have a List of contacts.
I'm trying to link my Contact Methods so that it'll be populated with the correct Person_Id
Example
Id Name Method Value Person_Id
1 John Phone 7777777777 2
2 Joan Phone 8888888888 8
3 Jack Phone 9999999999 9
Currently it displays Person_Id as all nulls, I believe I didn't create my ContactMethod Class correctly. If I can get help establishing a proper foreign key. I think that's my issue.
// Primary Keys -------------------------------------------------------
public int Id { get; set; }
// Associations -------------------------------------------------------
public virtual Person Person { get; set; }
// Fields -------------------------------------------------------------
public string Name {get; set;
public string Value { get; set; }
I populate the information through a migration script, here's a snippet
var person = people.Find(x => x.ContactBase_GUID == contactBaseGuid);
contactMethods.AddRange(channels.Select(pair => new { pair, method = reader.Get<string>(pair.Key) })
.Where(x => x.method != null)
.Select(x => new ContactMethod { Value = x.method, Person = person }));
Working Method not utilizing foreign keys.
ContactMethod Class
// Associations -------------------------------------------------------
public int? Person_Id { get; set; }
MigrationScript
var person = people.Find(x => x.ContactBase_GUID == contactBaseGuid);
contactMethods.AddRange(channels.Select(pair => new { pair, method = reader.Get<string>(pair.Key) })
.Where(x => x.method != null)
.Select(x => new ContactMethod { Value = x.method, Person = person.Id }));
I'm going to suppose that you have a model like this:
public class Contact
{
public int Id { get; set; }
public virtual Person Person { get; set; }
public string Name { get; set; }
public string Value { get; set; }
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Contact> Contacts { get; set; }
}
Now,to achieve the escenario that you want, you need to configure a one-to many relationship between Contact and Person. There are two ways to do that, using Data Annotations or using Fluent Api. I'm going to use Fluent Api in this case. The easy way is override the OnModelCreating method of your Context to configure the relationship, for example, at this way:
public class YourContext : DbContext
{
//...
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Person>()
.HasMany(p => p.Contacts)
.WithRequired(c => c.Person)
.Map(f => f.MapKey("Person_Id"));
}
}
As you can see, I'm specifying a PK that is not declared in your Contact class, that is the escenario that you want. Now with this configuration, you could do this, for example:
Person john=new Person(){Name = "John"};
var contactList = new List<Contact>() {new Contact(){Name = "Phone",Value = "555-444-333",Person = john},
new Contact() { Name = "email", Value = "john#gmail.com",Person = john}};
using (YourContext db = new YourContext())
{
db.Contacts.AddRange(contactList);
db.SaveChanges();
}
Update
If you want to do the same configuration using Data Annotations, your model would be like this:
public class Contact
{
[Key]
public int Id { get; set; }
[InverseProperty("Contacts")]
public virtual Person Person { get; set; }
public string Name { get; set; }
public string Value { get; set; }
}
public class Person
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Contact> Contacts { get; set; }
}
If you want to use a FK related to Person in your Contact class, you could do this:
public class Contact
{
[Key]
public int Id { get; set; }
public int? Person_Id { get; set; }
[ForeignKey("Person_Id"), InverseProperty("Contacts")]
public virtual Person Person { get; set; }
public string Name { get; set; }
public string Value { get; set; }
}
This way you can use directly the Person_Id FK in your second migration script:
var person = people.Find(x => x.ContactBase_GUID == contactBaseGuid);
contactMethods.AddRange(channels.Select(pair => new { pair, method = reader.Get<string>(pair.Key) })
.Where(x => x.method != null)
.Select(x => new ContactMethod { Value = x.method, Person_Id = person.Id }));