AutoMapper relations between two entities - c#

I have two entity models in my ASP.NET Core 6 application:
public partial class Employee
{
public int Id { get; set; }
public string SerialNumber { get; set; } = null!;
public bool Active { get; set; }
public int? FKIdClassEmployee { get; set; }
public virtual ClassEmployee? ClassEmployee { get; set; }
}
public partial class ClassEmployee
{
public ClassEmployee()
{
Employee = new HashSet<Employee>();
}
public int Id { get; set; }
public string? Label { get; set; }
public decimal? Cost { get; set; }
public virtual ICollection<Employee> Employees { get; set; }
}
In the EmployeeController, I'm try to use AutoMapper to map ClassEmployee to an existing record.
This is my Automapper config:
public class EmployeeProfile : Profile
{
private readonly ApplicationContext _ctx;
public EmployeeProfile(ApplicationContext ctx)
{
_ctx = ctx;
EmployeeDTOToEmployee();
}
private void EmployeeDTOToEmployee()
{
CreateMap<EmployeeDTO, Employee>()
.ForMember(dest => dest.SerialNumber, opt => opt.MapFrom(d => d.SerialNumber))
.ForMember(dest => dest.Active, opt => opt.MapFrom(d => d.Active))
.ForMember(dest => FKIdClassEmployee, opt => opt.MapFrom(d => d.FKIdClassEmployee))
.ForMember(dest => dest.ClassEmployee, opt => opt.MapFrom(
src => _ctx.ClassEmployee.Find(src.FKIdClassEmployee)));
}
}
But it doesn't work because it returns an error
Duplicate key value violates unique constraint
when it is called the Create method.

First of all involving DbContext in mapping configuration isn't a good idea (in my opinion). Use Lazy, Eager or Explicit loading instead. If a pair of Model (Entity) and Dto has same property name and type you don't have to specify .ForMemeber(dest => dest..., opt => opt.MapFrom(...)) . For nested mapping just configure all Entities and Dtos.
Configuration should look like this:
// NOTE: Same property type and name will be mapped
// Map Dto values to Entity
CreateMap<EmployeeDTO, Employee>();
CreateMap<ClassEmployeeDTO, ClassEmployee>();
// Map Entity values to Dto
CreateMap<Employee, EmployeeDTO>();
CreateMap<ClassEmployee, ClassEmployeeDTO>();

Related

EF Core - many to many relationship use / access custom join table

I am trying to implement a many to many relationship.
The Models -
public class User
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public List<Book> OwnedBooks { get; set; }
}
public class Own
{
public int UserId { get; set; }
public int BookId { get; set; }
public User User { get; set; }
public Book Book { get; set; }
}
public class Book
{
[Key]
public int Id { get; set; }
public int AuthorId { get; set; }
public User Author { get; set; }
public List<User> OwnedBy { get; set; } //Not really needed, but without it I can't create the join table "Own"
[NotMapped]
public int UsersReached; //Get this via the "Own" table
}
The DbContext -
public class TestContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Book> Books { get; set; }
public DbSet<Own> Own { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlServer("Server=DESKTOP-BT4H8CA;Database=Test;Trusted_Connection=True");
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Book>().HasOne(x => x.Author);
builder.Entity<User>()
.HasMany(x => x.OwnedBooks)
.WithMany(x => x.OwnedBy)
.UsingEntity(x => x.ToTable("Own"));
builder.Entity<Own>()
.HasKey(x => new {x.BookId, x.UserId});
}
}
I am struggling with accessing the join table "Own". I need it to get the amount of each Book that is sold, without completely loading the users. That's why I don't want to use the auto generated one:
Cannot use table 'Own' for entity type 'BookUser (Dictionary<string, object>)' since it is being used for entity type 'Own' and potentially other entity types, but there is no linking relationship. Add a foreign key to 'BookUser (Dictionary<string, object>)' on the primary key properties and pointing to the primary key on another entity typed mapped to 'Own'.
Thanks in advance for your help!
You can actually use the auto-generated joining table and still get the count of each book sold, without completely loading the users.
With your current User and Book models, configure the relationships as -
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Book>()
.HasOne(p => p.Author)
.WithMany()
.HasForeignKey(p => p.AuthorId)
.OnDelete(DeleteBehavior.NoAction);
builder.Entity<User>()
.HasMany(p => p.OwnedBooks)
.WithMany(p => p.OwnedBy);
}
Then you can query the books with their count of sales as -
var books = dbCtx.Books
.Select(p => new Book
{
Id = p.Id,
AuthorId = p.AuthorId,
Author = p.Author,
UsersReached = p.OwnedBy.Count // this will not load User entities
})
.ToList();
EDIT:
You can use AutoMapper which can do the projection in .Select() method for you, like -
var dtos = _Mapper.ProjectTo<BookDTO>(dbCtx.Books).ToList();
For that, you'll need to -
create a DTO model with properties you want from the query result, like -
public class BookDTO
{
public int Id { get; set; }
public string Author { get; set; }
public int UsersReached { get; set; }
}
define a map from Book to BookDTO -
CreateMap<Book, BookDTO>()
.ForMember(d => d.Author, opt => opt.MapFrom(s => s.Author.Name))
.ForMember(d => d.UsersReached, opt => opt.MapFrom(s => s.OwnedBy.Count));
You can remove the [NotMapped] property UsersReached from the Book model.

Entity Framework One to Many with no Collection navigation property

In Entity Framework with Fluent Configuration, I have a LeaseTrackingType entity which has a one to many relationship with LeaseTracking. Where each lease tracking has a lease tracking type.
However from a code point of view it doesn't really make sense to have a LeaseTrackings collection. Like you are never going to use this navigation property.
Question: How do I model the one to many relationship without LeaseTrackings collection navigation property?
Entities:
public class LeaseTracking
{
public int Id { get; set; }
public int LeaseTrackingTypeId { get; set; }
public LeaseTrackingType LeaseTrackingType { get; set; }
}
public class LeaseTrackingType
{
public int Id { get; set; }
public string Name { get; set;}
public virtual Collection<LeaseTracking> LeaseTrackings { get; set;}
}
And mapping configuration:
public class LeaseTrackingConfiguration : EntityTypeConfiguration<LeaseTracking>
{
public LeaseTrackingConfiguration()
{
ToTable("LeaseTracking");
Property(entity => entity.Id);
HasRequired(entity => entity.LeaseTrackingType)
.WithMany(entity => entity.LeaseTrackings)
.HasForeignKey(entity => entity.LeaseTrackingTypeId);
}
}
public class LeaseTrackingTypeConfiguration : EntityTypeConfiguration<LeaseTrackingType>
{
public LeaseTrackingTypeConfiguration()
{
ToTable("LeaseTrackingType");
Property(entity => entity.Id);
Property(entity => entity.Name).;
}
}
in the constructor don't point it out in WithMany function and get rid of the virtual Collection in LeaseTrackingType class
public LeaseTrackingConfiguration()
{
ToTable("LeaseTracking");
Property(entity => entity.Id);
HasRequired(entity => entity.LeaseTrackingType)
.WithMany()
.HasForeignKey(entity => entity.LeaseTrackingTypeId);
}

Automapper ignore nested collection properties

I have a simple objects:
public class Project : Entity
{
public uint ProjectId { get; set; }
public virtual ICollection<Cabin> Cabins { get; set; }
}
public class Cabin : Entity
{
public IPAddress IpAddress { get; set; }
public int Port { get; set; }
public DateTime LastConnection { get; set; }
public byte ConnectionStatus { get; set; }
public byte TechnicalStatus { get; set; }
public Project Project { get; set; }
public int ProjectId { get; set; }
}
So mapping using auto mapper from one to another with some ignores would look like:
var mapperConfig = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Project, Project>()
.ForMember(source => source.Id, opt => opt.Ignore())
.ForMember(source => source.ProjectId, opt => opt.Ignore())
.ForMember(source => source.Cabins, opt => opt.MapFrom(cab => cab.cabins));
});
And it works it maps one project object to another, and ignores id and project id and maps collection.
But on that level, is it possible to set what properties from source.Cabins would be ignored?
For example i want to ignore ConnectionStatus, TechnicalStatus.
You could add a configuration mapping for Cabin entity and AutoMapper would look at these configurations before mapping Cabin entity.
cfg.CreateMap<Cabin, Cabin>()
.ForMember(source => source.ConnectionStatus, opt => opt.Ignore())
.ForMember(source => source.TechnicalStatus, opt => opt.Ignore());
Or you could use AfterMap event to define a default value for these properties.

Getting related data via an AutoMapper mapping?

I want to create a mapping between this entity model:
public class ProductType
{
public int Id { get; set; }
public string Title { get; set; }
public int SortOrder { get; set; }
public ICollection<ProductIdentifierInType> Identifiers { get; set; }
public ICollection<ProductPropertyInType> Properties { get; set; }
public ICollection<Product> Products { get; set; }
}
... and this viewmodel:
public class ViewModelProductType
{
public int Id { get; set; }
public string Title { get; set; }
public int SortOrder { get; set; }
public IList<ViewModelProductIdentifier> Identifiers { get; set; }
public IList<ViewModelProductProperty> Properties { get; set; }
public ICollection<ViewModelProduct> Products { get; set; }
}
... but since the Identifiers and Properties are not of the same type in the viewmodel as in the entity model, it won't work directly, like this:
CreateMap<ProductType, ViewModelProductType>();
I don't want to change my models too much. In the entity model, I need the Identifiers and Properties to be respectively ProductIdentifierInType and ProductPropertyInType, because there are many-to-many relationships there, which requires linking tables.
But in the viewmodel, I need Identifiers and Properties to be the full objects in order to display their properties in the view.
Is there a way to accomplish this with mapping? Maybe using .ForPath() to get the two objects' properties?
Assuming you have defined the direct entity to view model mappings:
CreateMap<ProductIdentifier, ViewModelProductIdentifier>();
CreateMap<ProductProperty, ViewModelProductProperty>();
Now it would be enough to extract the corresponding member using LINQ Select inside MapFrom expression. The important thing to know is that AutoMapper does not require the type of the returned expression to match the type of the destination. If they don't match, AutoMapper will use the explicit or implicit mappings for that types.
CreateMap<ProductType, ViewModelProductType>()
.ForMember(dst => dst.Identifiers, opt => opt.MapFrom(src =>
src.Identifiers.Select(link => link.Identifier)))
.ForMember(dst => dst.Properties, opt => opt.MapFrom(src =>
src.Properties.Select(link => link.Property)))
;
I think what you are looking for is a Custom Value Resolver.
There you can explicitly specify how Auto Mapper should map one object to another.
In your case it could look something like this:
public class CustomResolver : IValueResolver<ProductType, ViewModelProductType, IList<ViewModelProductIdentifier>>
{
public int Resolve(ProductType source, ViewModelProductType destination, IList<ViewModelProductIdentifier> destMember, ResolutionContext context)
{
// Map you source collection to the destination list here and return it
}
}
You can then pass/inject the resolver when calling CreateMap, i.e.:
CreateMap<ProductType, ViewModelProductType>()
.ForMember(dest => dest.Identifiers, opt => opt.ResolveUsing<CustomResolver>());
Analogously, do the same for your 'Properties' property.
Note that I did not debug this but merely adapted the examples provided in the link above.

Automapper Config: Can I use .ForAllMembers when overriding CreateMap?

public class MyProfile : Profile
{
protected override void Configure()
{
base.CreateMap<ViewModel, Domain>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
//.ForAllMembers(opt => opt.Ignore()) //returns void
.ReverseMap();
}
}
public class ViewModel
{
public int Id { get; set; }
}
public class Domain
{
public int Id { get; set; }
public string UserName {get; set;}
//public string ... { get; set;} //etc..
//...
}
Suppose I don't want to map UserName, and many other properties.
Can I do .ForAllMembers(...) to the mapping, in order to map any unmapped members?
Don't use that ForAllMembers thing, that looks like a version of this:
https://github.com/AutoMapper/AutoMapper/wiki/5.0-Upgrade-Guide#ignoreallnonexisting-extension
Instead, use the CreateMap overload that takes a MemberList enum:
CreateMap<ViewModel, Domain(MemberList.None)

Categories