Automapper ignore nested collection properties - c#

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.

Related

AutoMapper relations between two entities

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

How can I include two relational properties of the same type?

I have an ApplicationUser model:
public class ApplicationUser : IdentityUser
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
public List<Project> Projects { get; set; }
}
... and a Project model:
public class Project
{
public int Id { get; set; }
public int? ParentProjectId { get; set; }
[Required]
public string ProjectCreatorId { get; set; }
[Required]
public string ProjectOwnerId { get; set; }
public string Title { get; set; }
public DateTime Created { get; set; }
public ApplicationUser ProjectCreator { get; set; }
public ApplicationUser ProjectOwner { get; set; }
public Project ParentProject { get; set; }
public virtual List<Project> ChildProjects { get; set; }
}
In OnModelCreating(), I tried this:
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Project>()
.HasMany(c => c.ChildProjects)
.WithOne(p => p.ParentProject)
.HasForeignKey(p => p.ParentProjectId);
modelBuilder.Entity<ApplicationUser>()
.HasMany(p => p.Projects)
.WithOne(o => o.ProjectOwner)
.HasForeignKey(po => po.ProjectOwnerId);
modelBuilder.Entity<ApplicationUser>()
.HasMany(p => p.Projects)
.WithOne(c => c.ProjectCreator)
.HasForeignKey(pc => pc.ProjectCreatorId);
But upon creating the database, I get
Cannot create a relationship between 'ApplicationUser.Projects' and 'Project.ProjectCreator', because there already is a relationship between 'ApplicationUser.Projects' and 'Project.ProjectOwner'. Navigation properties can only participate in a single relationship.
I tried the solutions to this old question, but wasn't able to make any of them work.
Is there another way I could keep track of both a Project's creator AND owner, and be able to .Include() them both in the queries?
Instead of different binding, use the single binding
modelBuilder.Entity<ApplicationUser>()
.HasMany(p => p.Projects)
.WithOne(o => o.ProjectOwner)
.HasForeignKey(po => po.ProjectOwnerId);
.WithOne(c => c.ProjectCreator)
.HasForeignKey(pc => pc.ProjectCreatorId);
I went for this solution:
As mentioned in the comments, ApplicationUser would need two List<Project>-properties:
public List<Project> CreatedProjects { get; set; }
public List<Project> OwnedProjects { get; set; }
Then, in OnModelCreating():
modelBuilder.Entity<Project>()
.HasOne(g => g.ProjectCreator)
.WithMany(t => t.CreatedProjects)
.HasForeignKey(t => t.ProjectCreatorId)
.HasPrincipalKey(t => t.Id);
modelBuilder.Entity<Project>()
.HasOne(g => g.ProjectOwner)
.WithMany(t => t.OwnedProjects)
.HasForeignKey(t => t.ProjectOwnerId).OnDelete(DeleteBehavior.NoAction)
.HasPrincipalKey(t => t.Id);
Now I'm able to include both creators and owners. :)

EF Core: Invalid column name 'companyId1'

I have trouble understanding why a EF generated SELECT clause contains the primary key twice, the second one is postfixed with '1'.
exec sp_executesql N'SELECT [entity.WebAdminCompanyUser].[CompanyId], [entity.WebAdminCompanyUser].[AspNetUserId], [entity.WebAdminCompanyUser].[CompanyId1]
FROM [SafeProtect].[WebAdminCompanyUser] AS [entity.WebAdminCompanyUser]
INNER JOIN (
SELECT [entity1].[AspNetUserId]
FROM [SafeProtect].[WebAdminUser] AS [entity1]
WHERE ([entity1].[RowEnabled] = 1) AND EXISTS (
SELECT 1
FROM [SafeProtect].[WebAdminCompanyUser] AS [companyUser1]
WHERE ([companyUser1].[CompanyId] = #__companyId_0) AND ([entity1].[AspNetUserId] = [companyUser1].[AspNetUserId]))
) AS [t0] ON [entity.WebAdminCompanyUser].[AspNetUserId] = [t0].[AspNetUserId]
ORDER BY [t0].[AspNetUserId]',N'#__companyId_0 int',#__companyId_0=1
It fails with Invalid column name 'CompanyId1'.
Following are the entities and the corresponding configurations (fluent API):
WebAdminCompanyUser:
public partial class WebAdminCompanyUser : ITrackable, IMergeable
{
public WebAdminCompanyUser()
{
AdditionalInit();
}
public int CompanyId { get; set; }
public int AspNetUserId { get; set; }
public virtual Company Company { get; set; }
[NotMapped]
public TrackingState TrackingState { get; set; }
[NotMapped]
public ICollection<string> ModifiedProperties { get; set; }
[NotMapped]
public Guid EntityIdentifier { get; set; }
partial void AdditionalInit();
}
}
Configuration:
builder.Entity<WebAdminCompanyUser>(entity =>
{
entity.ToTable(name: "WebAdminCompanyUser", schema: SqlSchema.SafeProtect);
entity.HasKey("CompanyId", "AspNetUserId");
entity
.HasOne(d => d.Company)
.WithMany()
.HasForeignKey(d => d.CompanyId)
.IsRequired();
});
WebAdminUser:
public partial class WebAdminUser : IdentityUser<int>, IAuditInfo, IRowDisableableWithDateTime, ITrackable, IMergeable
{
public WebAdminUser()
{
WebAdminCompanyUser = new HashSet<WebAdminCompanyUser>();
WebAdminUserRole = new HashSet<WebAdminUserRole>();
WebAdminUserClaim = new HashSet<WebAdminUserClaim>();
WebAdminUserLogin = new HashSet<WebAdminUserLogin>();
AdditionalInit();
}
public string DisplayName { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime? ModifiedOn { get; set; }
public bool RowEnabled { get; set; }
public DateTime? DisabledOn { get; set; }
public virtual ICollection<WebAdminCompanyUser> WebAdminCompanyUser { get; set; }
public virtual ICollection<WebAdminUserRole> WebAdminUserRole { get; set; }
public virtual ICollection<WebAdminUserClaim> WebAdminUserClaim { get; set; }
public virtual ICollection<WebAdminUserLogin> WebAdminUserLogin { get; set; }
[NotMapped]
public TrackingState TrackingState { get; set; }
[NotMapped]
public ICollection<string> ModifiedProperties { get; set; }
[NotMapped]
public Guid EntityIdentifier { get; set; }
partial void AdditionalInit();
}
Configuration:
builder.Entity<WebAdminUser>(entity =>
{
entity.ToTable(name: "WebAdminUser", schema: SqlSchema.SafeProtect);
entity.Property(e => e.Id).HasColumnName("AspNetUserId");
// authorize multiple user name
entity.HasIndex((p) => new { p.UserName }).IsUnique(false);
entity
.HasMany(user => user.WebAdminUserClaim)
.WithOne()
.HasForeignKey(userClaims => userClaims.UserId)
.IsRequired();
entity
.HasMany(user => user.WebAdminUserLogin)
.WithOne()
.HasForeignKey(userLogin => userLogin.UserId)
.IsRequired();
entity
.HasMany(user => user.WebAdminUserRole)
.WithOne()
.HasForeignKey(userRole => userRole.UserId)
.IsRequired();
entity
.HasMany(user => user.WebAdminCompanyUser)
.WithOne()
.HasForeignKey(companyUser => companyUser.AspNetUserId)
.IsRequired();
});
EF query:
IQueryable<WebAdminUser> query =
from WebAdminUser user WebAdminUserRepository.All()
.Include(user => user.WebAdminUserRole)
.ThenInclude(userRole => userRole.AspNetRole)
.Include(user => user.WebAdminCompanyUser)
where user.WebAdminCompanyUser.Any(companyUser => companyUser.CompanyId == companyId)
select user;
return query.ToList();
Any help appreciated.
This usually happens when you have improperly mapped relationship by leaving some navigation property out of fluent configuration.
Remember that each navigation property (collection or reference) represents a relationship. If you fluently configure relationships and use HasOne / HasMany / WithOne / WithMany w/o passing the navigation property, you are telling EF that the relationship has no navigation property for the corresponding end. But if you actually do have navigation property, EF will map it to a separate relationship with default FK column name. If the default property/column name is already used, EF will append index to it until it gets unique.
In your case, the WebAdminUser class and configuration you've shown are irrelevant. The invalid column name CompanyId1 indicates that the problem is with Company class which you haven't shown, and the WithMany() call here
.HasOne(d => d.Company)
.WithMany() // <--
Most likely your Company class has collection navigation property to WebAdminCompanyUser, something like this (virtual and the name of the property doesn't matter):
public virtual ICollection<WebAdminCompanyUser> CompanyUsers { get; set; }
then you need to change the above .WithMany() call with something like
.WithMany(c => c.CompanyUsers)
and the problem will be solved.

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)

Prevent AutoMapper ProjectTo from adding unwanted column

It seems to me that using AutoMapper ProjectTo<> is adding an unwanted (calculated) column to my query. Here's the query:
SELECT TOP(1) CASE
WHEN [dto].[Id] IS NULL
THEN CAST(0 AS BIT) ELSE CAST(1 AS BIT)
END, [dto].[Enabled], [dto].[DurationWarningThresholdSec], [dto].[AverageDurationLabel]
where that first CASE & CAST unnamed column is not really needed, I think. This is the gist of my issue.
This is the EF Core (maybe that matters) POCO entity being queried:
public class CountersConfigData
{
public Guid Id { get; set; }
public bool Enabled { get; set; }
public int DurationWarningThresholdSec { get; set; }
public string AverageDurationLabel { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ModifiedAt { get; set; }
public DateTime? DeletedAt { get; set; }
public Guid CompanyId { get; set; }
}
// within DbContext.OnModelCreating()
entityTypeBuilder.HasKey(cfg => cfg.Id);
entityTypeBuilder.HasIndex(cfg => cfg.DeletedAt);
Here is the destination shape of mapping:
public class Result
{
public bool Existing { get; set; }
public CountersMainConfig Main { get; set; }
}
public class CountersMainConfig
{
public bool Enabled { get; set; }
public int DurationWarningThresholdSec { get; set; }
public string AverageDurationLabel { get; set; }
}
This is the mapping initial setup:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<CountersConfigData, Result>()
.ForMember(dest => dest.Main, opt => opt.MapFrom(src => src))
.ForMember(dest => dest.Existing, opt => opt.Ignore());
cfg.CreateMap<CountersConfigData, CountersMainConfig>();
});
I understand that I could project to inner type, CountersMainConfig, and then manually create the outer type instance. But I have further cases similar to this one, with more complicated outer types, so I'd like to sort this out for all of them.
If I actually project to the inner type, that first column goes away from generated query. What am I missing? TA
EDIT I created a simplified solution just to confirm the issue before asking here. Afterwards I also tried changing outer destination class by making Existing a float, or by removing it, with no luck.
The issue is similar to Automapper Projection with Linq OrderBy child property error, so is the solution - configure AutoMapper to not generate null check for the Main property:
.ForMember(dest => dest.Main, opt => { opt.AllowNull(); opt.MapFrom(src => src); })

Categories