Mapping properties from base class doesn't work using ExplicitExpansion - c#

AutoMapper is not projecting query in EF when using ExplicitExpansion in a property that is defined in a base class. The following code shows the models:
/*********** Source Types ***********/
class EntityBase
{
public Guid? CreatedById { get; set; }
public Guid? ModifiedById { get; set; }
[ForeignKey("CreatedById")]
public User CreatedBy { get; set; }
[ForeignKey("ModifiedById")]
public User ModifiedBy { get; set; }
}
class User
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
}
class Script : EntityBase
{
[Key]
public Guid Id { get; set; }
public string Text { get; set; }
}
/*********** Destination Types ***********/
class EntityBaseModel
{
public Guid? CreatedById { get; set; }
public Guid? ModifiedById { get; set; }
public UserModel CreatedBy { get; set; }
public UserModel ModifiedBy { get; set; }
}
class UserModel
{
public Guid Id { get; set; }
public string Name { get; set; }
}
class ScriptModel : EntityBaseModel
{
public Guid Id { get; set; }
public string Text { get; set; }
public new UserModel ModifiedBy { get; set; } //notice the 'new' here? this will work
}
I'm using the following mapping configuration:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Script, ScriptModel>()
.MaxDepth(1)
.ForMember(d => d.ModifiedBy, o => o.ExplicitExpansion())
.ForMember(d => d.CreatedBy, o => o.ExplicitExpansion());
});
The following test will fail, but it shouldn't... it should fetch the CreatedBy property because I've explicitly said so:
/// <summary>
/// This will fail, but it shouldn't.
/// </summary>
[TestMethod]
public void Should_Map_CreatedBy()
{
using (var context = new MyContext())
{
var model = context.Scripts.Include("CreatedBy").ProjectTo<ScriptModel>(null, "CreatedBy").FirstOrDefault();
Assert.IsNotNull(model);
Assert.IsNotNull(model.CreatedBy);
}
}
I've also tried these configurations, but it doesn't work either
Mapper.Initialize(cfg =>
{
cfg.CreateMap<EntityBase, EntityBaseModel>();
cfg.CreateMap<User, UserModel>();
cfg.CreateMap<Script, ScriptModel>()
.MaxDepth(1)
.ForMember(d => d.ModifiedBy, o => o.ExplicitExpansion())
.ForMember(d => d.CreatedBy, o => o.ExplicitExpansion());
});
Mapper.Initialize(cfg =>
{
cfg.CreateMap<EntityBase, EntityBaseModel>();
cfg.CreateMap<User, UserModel>();
cfg.CreateMap<Script, ScriptModel>()
.IncludeBase<EntityBase, EntityBaseModel>()
.MaxDepth(1)
.ForMember(d => d.ModifiedBy, o => o.ExplicitExpansion())
.ForMember(d => d.CreatedBy, o => o.ExplicitExpansion());
});
Is there something wrong with my mapping configuration? Is that an automapper bug?
Here's the full repro: https://1drv.ms/u/s!AhH0QYI81F61gtIx0q27BZ05EM-xQA

cfg.CreateMap<User, UserModel>();
cfg.CreateMap<Script, ScriptModel>()
.ForMember(d => d.ModifiedBy, o => o.ExplicitExpansion())
.ForMember(d => d.CreatedBy, o => o.ExplicitExpansion());
var model = context.Scripts.ProjectTo<ScriptModel>(s=>s.CreatedBy).FirstOrDefault()
This works for me with the latest version.

Related

Automapper Map All Members of Certain Destination Type

I'm loading data from a third-party API where one of the objects contain a list of attributes that can be one of several values. For example:
{
"name": "blah",
"permissions": {
"action_1": "allowed",
"action_2": "not_allowed",
"action_3": "allowed",
"action_4": "not_allowed",
"action_5": "limited",
"action_6": "limited"
}
}
The value for each permission can be allowed, not_allowed, or limited. My API response permissions model has these mapped to strings for simplicity, but I need to map these to an Enum when converting to my data model. I know I can map each property individually like so:
public class MyProfile : Profile {
public MyProfile()
{
CreateMap<ApiResponsePermissions, Permissions>()
.ForMember(dest => dest.Action1, o => o.MapFrom(s => MapPermission(s.Action1)))
.ForMember(dest => dest.Action2, o => o.MapFrom(s => MapPermission(s.Action2)))
.ForMember(dest => dest.Action3, o => o.MapFrom(s => MapPermission(s.Action3)))
.ForMember(dest => dest.Action4, o => o.MapFrom(s => MapPermission(s.Action4)))
.ForMember(dest => dest.Action5, o => o.MapFrom(s => MapPermission(s.Action5)))
.ForMember(dest => dest.Action6, o => o.MapFrom(s => MapPermission(s.Action6)));
}
static PermissionEnum MapPermission(string permission)
{
// ...
}
}
I also know I can use ForAllMembers to map every property, but can that be used to map specific properties based on the destination type? The actual destination class has other properties besides the permissions.
Relevant Classes:
public class Permissions {
public int Id { get; set; }
public int ParentId { get; set; }
public virtual Parent Parent { get; set; }
public PermissionEnum Action1 { get; set; }
public PermissionEnum Action2 { get; set; }
public PermissionEnum Action3 { get; set; }
public PermissionEnum Action4 { get; set; }
public PermissionEnum Action5 { get; set; }
public PermissionEnum Action6 { get; set; }
}
public class ApiResponsePermissions {
public string Action1 { get; set; }
public string Action2 { get; set; }
public string Action3 { get; set; }
public string Action4 { get; set; }
public string Action5 { get; set; }
public string Action6 { get; set; }
}
public enum PermissionEnum {
NotAllowed,
Allowed,
Limited
}

AutoMapper Infinite Loop using EF Code First

I have the following classes (One-One relationship Asset-TrackingDevice):
public class Asset
{
public int Id { get; set; }
public string Name { get; set; }
public TrackingDevice TrackingDevice { get; set; }
}
public class TrackingDevice
{
public int Id { get; set; }
public string Imei { get; set; }
public int? AssetId { get; set; }
public Asset Asset { get; set; }
}
The viewModels are very similar:
public class AssetViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public int? TrackingDeviceId { get; set; }
public TrackingDeviceViewModel TrackingDevice { get; set; }
}
public class TrackingDeviceViewModel
{
public int Id { get; set; }
public string Imei { get; set; }
public AssetViewModel Asset { get; set; }
public string AssetId { get; set; }
}
Mappings:
CreateMap<Asset, AssetViewModel>()
.ForMember(d => d.TrackingDevice, map => map.Ignore());
CreateMap<AssetViewModel, Asset>()
.ForMember(d => d.TrackingDevice, map => map.Ignore());
CreateMap<AssetViewModel, Asset>()
.ReverseMap();
CreateMap<TrackingDevice, TrackingDeviceViewModel>()
.ForMember(d => d.Asset, map => map.Ignore());
CreateMap<TrackingDeviceViewModel, TrackingDevice>()
.ForMember(d => d.Asset, map => map.Ignore());
CreateMap<TrackingDevice, TrackingDeviceViewModel>()
.ReverseMap();
When I perform a database query to obtain the TrackingDevices,
I get an error because in the mapping the Asset within Tracking Device also includes a Tracking Device and so on.
The query that I execute to obtain the tracking devices is:
var trackingDevices = _appContext.TrackingDevices
.Include(td => td.Asset)
.ToListAsync();
var trackingMapper = Mapper.Map<IEnumerable<TrackingDeviceViewModel>>(trackingDevices);
I read that by including the Map.Ignore would fix the problem but it did not work either, does anyone know what my error is?

Mapping to nested value Automapper

I'm struggling to map 2 objects. Basically have Product which is my EF model, and I'm mapping this to ProductDto, which has FileDto.
I'd like to map Product.FileName to ProductDto.File.Internal name, how to do this?
Classes below.
public class Product : BaseEntity<long>
{
[MaxLength(100)]
public string Name { get; set; }
[MaxLength(100)]
public string Barcode { get; set; }
public int ShelfLife { get; set; }
public int Weight { get; set; }
public bool HasAllergens { get; set; }
[MaxLength(100)]
public string FileName { get; set; }
[ForeignKey("Id")]
public int CustomerId { get; set; }
public virtual ICollection<ProductIngredient> ProductIngredient { get; set; }
public virtual ICollection<Nutrition> Nutritions { get; set; }
public virtual ICollection<ProductComposition> Composition { get; set; }
public virtual IList<ProductionProcess> ProductionProcess { get; set; }
}
public class ProductDto
{
public long Id { get; set; }
public DateTime CretedOn { get; set; }
public DateTime UpdatedOn { get; set; }
public string Name { get; set; }
public string Barcode { get; set; }
public int ShelfLife { get; set; }
public int Weight { get; set; }
public bool HasAllergens { get; set; }
public int CustomerId { get; set; }
public FileDto File { get; set; }
public IList<IngredientDto> Ingredients { get; set; }
public IList<NutritionDto> Nutritions { get; set; }
public IList<ProductCompositionDto> Composition { get; set; }
public IList<ProductionProcessDto> ProductionProcess { get; set; }
}
public class ProductionProcessDto
{
public string Key { get; set; }
public string Value { get; set; }
public FileDto File { get; set; }
}
public class NutritionDto
{
public string Key { get; set; }
public string Value { get; set; }
}
public class ProductCompositionDto
{
public string Key { get; set; }
public string Value { get; set; }
}
File Dto:
public class FileDto
{
public string Base64EncodedFile { get; set; }
public string OriginalName { get; set; }
public string InternalName { get; set; }
public string Type { get; set; }
}
Automapper so far:
//Product
CreateMap<Nutrition, NutritionDto>().ReverseMap();
CreateMap<ProductComposition, ProductCompositionDto>().ReverseMap();
CreateMap<ProductionProcessDto, ProductionProcess>()
.ForMember(dest => dest.FileInternalName, opt => opt.MapFrom(src => src.File.InternalName))
.ForMember(dest => dest.FileOriginalName, opt => opt.MapFrom(src => src.File.OriginalName))
.ReverseMap();
CreateMap<Product, ProductDto>()
.ForMember(d => d.File, o => o.MapFrom(s => Mapper.Map<Product, FileDto>(s)))
.ForMember(d => d.Nutritions, o => o.MapFrom(s => s.Nutritions))
.ForMember(d => d.Composition, o => o.MapFrom(s => s.Composition))
.ForMember(d => d.ProductionProcess, o => o.MapFrom(s => s.ProductionProcess))
.ForMember(d => d.Ingredients, o => o.MapFrom(s => s.ProductIngredient.Select(pi => pi.Ingredients)))
.ReverseMap();
CreateMap<ProductDto, Product>()
.ForMember(d => d.FileName, o => o.MapFrom(s => s.File.InternalName))
.ReverseMap();
I am able to map from ProductDto (on data post) to Product but not other way around, all help much appreciated
Thanks
This code solved my issue:
.ForMember(d => d.File, o => o.MapFrom(model => new FileDto { InternalName = model.FileName }))
Applied to:
CreateMap<Product, ProductDto>()

Merge multiple sources into a single destination

I want to combine 2 Domain Objects into a single data transfer object using AutoMapper.
Domain Model:
public class Service {
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public ICollection<DownloadService> DownloadServices { get; set; } = new HashSet<DownloadService>();
}
public class DownloadService {
public int Id { get; set; }
public int PageLimit { get; set; }
public virtual int ServiceId { get; set; }
public virtual Service Service { get; set; }
}
public class Volume {
public override int Id { get; set; }
public bool IsActive { get; set; }
public string Path { get; set; }
public string Description { get; set; }
}
DTO:
public class PreferenceVM {
public ICollection<VolumeVM> Volumes { get; set; }
public ICollection<ServiceVM> Services { get; set; }
}
public class ServiceVM {
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public ICollection<DownloadServiceVM> DownloadServices { get; set; } = new HashSet<DownloadServiceVM>();
}
public class DownloadServiceVM {
public int Id { get; set; }
public int PageLimit { get; set; }
public int CleaningInterval { get; set; }
}
public class VolumeVM {
public int Id { get; set; }
public bool IsActive { get; set; }
public string Path { get; set; }
public string Description { get; set; }
}
Mapping:
cfg.CreateMap<Volume, VolumeVM>().ReverseMap();
cfg.CreateMap<DownloadService, DownloadServiceVM>().ReverseMap();
cfg.CreateMap<Service, ServiceVM>()
.ForMember(d => d.DownloadServices, opt => opt.MapFrom(s => s.DownloadServices))
.ReverseMap();
cfg.CreateMap<ICollection<Volume>, PreferenceVM>()
.ForMember(x => x.Volumes, y => y.MapFrom(src => src)).ReverseMap();
cfg.CreateMap<ICollection<Service>, PreferenceVM>()
.ForMember(x => x.Services, y => y.MapFrom(src => src)).ReverseMap();
when I try the mapping above:
var services = serviceRepository.GetAll();
var volumes = volumeRepository.GetAll();
var entities = mapper.Map<PreferenceVM>(services);
entities = mapper.Map(volumes, entities);
I get the following errors:
Missing type map configuration or unsupported mapping.
Mapping types: EntityQueryable1 -> PreferenceVM
Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable1[[Fwims.Core.Data.Model.Setting.Service,
Fwims.Core.Data.Model, Version=1.0.1.10, Culture=neutral,
PublicKeyToken=null]] -> Fwims.Core.ViewModel.Setting.PreferenceVM
It looks like my mapping is wrong, nothing I have tried has worked. How do I properly map the Domain objects to the Data transfer objects?
Here
cfg.CreateMap<ICollection<Volume>, PreferenceVM>()
.ForMember(x => x.Volumes, y => y.MapFrom(src => src)).ReverseMap();
and
cfg.CreateMap<ICollection<Service>, PreferenceVM>()
.ForMember(x => x.Services, y => y.MapFrom(src => src)).ReverseMap();
you create mappings from ICollection<TSource>.
However later on you are trying to map IQeryable<TSource>. While AutoMapper can use a base mapping to map a derived class, IQueryable<T> does not derive from ICollection<T>, hence the missing type map exception.
The solution is to create a mapping from some common base interface of IQueryable<T> and ICollection<T>, which is IEnumerable<T>.
So replace the above with:
cfg.CreateMap<IEnumerable<Volume>, PreferenceVM>()
.ForMember(x => x.Volumes, y => y.MapFrom(src => src));
cfg.CreateMap<IEnumerable<Service>, PreferenceVM>()
.ForMember(x => x.Services, y => y.MapFrom(src => src));
and the current issue will be solved.
Note that ReverseMap does not work in such scenarios, so I've just removed it. If you need such functionality, you have to create that mappings manually (eventually using ConvertUsing because there is no destination member).

Entity Framework - The foreign key component … is not a declared property on type

I have the following Model
public class FilanthropyEvent : EntityBase, IDeleteable
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime EventDate { get; set; }
public string Description { get; set; }
public decimal Target { get; set; }
public decimal EntryFee { get; set; }
public bool Deleted { get; set; }
public ICollection<EventAttendee> EventAttendees { get; set; }
}
public class Attendee : EntityBase, IDeleteable
{
public int Id { get; set; }
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public bool MailingList { get; set; }
public bool Deleted { get; set; }
public ICollection<EventAttendee> EventAttendees { get; set; }
}
Events and Attendees is a many to many relationship but I needed another property on the association so I created an association entity
public class EventAttendee : EntityBase
{
public int FilanthropyEventId { get; set; }
public int AttendeeId { get; set; }
public bool InActive { get; set; }
public virtual Attendee Attendee { get; set; }
public virtual FilanthropyEvent FilanthropyEvent { get; set; }
}
These are the configurations for each FilanthropyEvent and Attendee
public class FilanthropyEventConfiguration : EntityTypeConfiguration<FilanthropyEvent>
{
public FilanthropyEventConfiguration()
{
HasKey(x => x.Id);
Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasMany(x => x.EventAttendees).WithRequired(x => x.FilanthropyEvent).HasForeignKey(x => x.FilanthropyEvent);
}
}
public AttendeeConfiguration()
{
HasKey(x => x.Id);
Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasMany(x => x.EventAttendees).WithRequired(x => x.Attendee).HasForeignKey(x => x.AttendeeId);
}
public class EventAttendeesConfiguration : EntityTypeConfiguration<EventAttendee>
{
public EventAttendeesConfiguration()
{
HasKey(x => new {x.FilanthropyEventId, x.AttendeeId});
}
}
When I try and initialise the database via the update-database command in the package manager console I get the following error.
System.InvalidOperationException: The foreign key component 'FilanthropyEvent' is not a declared property on type 'EventAttendee'. Verify that it has not been explicitly excluded from the model and that it is a valid primitive property.
I realise I'm probably missing a mapping in the EventAttendeesConfiguration class but what would be the correct mapping to model this relationship?
This code
HasMany(x => x.EventAttendees)
.WithRequired(x => x.FilanthropyEvent)
.HasForeignKey(x => x.FilanthropyEvent);
Should be
HasMany(x => x.EventAttendees)
.WithRequired(x => x.FilanthropyEvent)
.HasForeignKey(x => x.FilanthropyEventId);

Categories