Automapper mapping collection of enums into view model collection - c#

AutoMapper 5.3.0 alpha (Yet to upgrade to the latest and greatest)
I have a DTO as follows:
public class AoACrudFieldValuesGdataHealthInput
{
public AoACrudFieldValuesGdataHealthInput()
{
PrevHealthAssessment = new HashSet<HealthPrevHealthAssessmentEnum>();
}
public HashSet<HealthPrevHealthAssessmentEnum> PrevHealthAssessment { get; set; }
}
and a EF POCO of:
[Table("Health", Schema = "gdata")]
public class GdataHealthTableModel : AuditedEntity
{
public GdataHealthTableModel()
{
//PrevHealthAssessment = new HashSet<HealthPrevHealthAssessmentEnum>();
}
[ForeignKey("Id")]
public ICollection<GdataHealthPrevAssesmentTableModel> PrevHealthAssessment { get; set; }
}
and:
[Table("HealthPrevAssesment", Schema = "gdata")]
public class GdataHealthPrevAssesmentTableModel : AuditedEntity
{
public HealthPrevHealthAssessmentEnum Assessment { get; set; }
}
I need help extending my map:
CreateMap<AoACrudFieldValuesGdataHealthInput, GdataHealthTableModel>();
such that AoACrudFieldValuesGdataHealthInput.PrevHealthAssessment ends up in GdataHealthTableModel.PrevHealthAssessment. Currently I get an error as my CreateMap() is not detailed enough - naturally.
Is this possible? Or do I get the mapper to ignore that field and do it by hand?
NOTE: For both EF POCO's I have omitted the Id field which is an auto increment for sake of brevity.

Define an additional map for HealthPrevHealthAssessmentEnum -> GdataHealthPrevAssesmentTableModel:
CreateMap<AoACrudFieldValuesGdataHealthInput, GdataHealthTableModel>()
.ForMember(dest => dest.PrevHealthAssessment, o => o.MapFrom(src => src.PrevHealthAssessment));
CreateMap<HealthPrevHealthAssessmentEnum, GdataHealthPrevAssesmentTableModel>()
.ForMember(dest => dest.Id, o => o.Ignore()) // auto ID
.ForMember(dest => dest.Assessment , o => o.MapFrom(src => src));
Not sure of AutoMapper can convert an ICollection to a HashSet out of the box, I think it can.

Related

Automapper: Mapping list to object

I am having problems mapping my domain object to my DTO object.
The error is:
Expression must resolve to top-level member and not any child object's properties. You can use ForPath, a custom resolver on the child type or the AfterMap option instead.
public class Sign
{
public List<Item> Items { get; set; } = new List<Item>();
}
public class SignDTO
{
public SignItemDTO Items { get; set; } = new SignItemDTO();
}
public class SignItemDTO
{
public List<ItemDTO> Items { get; set; } = new List<ItemDTO>();
}
private MapperConfiguration AutoMapperConfig()
{
return new MapperConfiguration(cfg =>
{
cfg.CreateMap<Item, ItemDTO>();
cfg.CreateMap<Sign, SignDTO>().ForPath(dest => dest.Items.Items, opt => opt.MapFrom(src => src.Items));
});
}
_context.Sign.Include(m => m.Items)
.ProjectTo<SignDTO>(AutoMapperConfig());
Probably it's due to your map in this line:
cfg.CreateMap<Sign, SignDTO>().ForPath(dest => dest.**Items.Items**, opt => opt.MapFrom(src => src.**Items**));
As you can see in between the **, you're maping from an entity property to another entity children's property, this is what AutoMapper does not like and what you see in the error message.
Maybe it was a mistake on your side an you can removed one of the Items or you can create a new mapping for your first level of Items which takes care of the childrens.

EF Automapper Parents child is null

I'm trying to read data from our database using entity framework and as the project already uses Automapper to convert from entities to Dtos it would make sense use Automappers Queryable Extensions to make life a bit easier. I'm using Microsoft.EntityFrameworkCore version 3.1.9.0
The problem is the returned array of BundleMetaDataDt.child is always null.
The query below returns plenty of data, but every BundleMetaDataDtos child value is null.
I have tried:
.Include(b => b.ChildBundle) before Where statement
.ForMember(dest => dest.ChildBundle, opt => opt.MapFrom(src => src.ChildBundleId))
.MaxDepth(2)
Classes: (There is more fields than shown below)
public partial class Bundle
{
public Guid? ChildBundleId { get; set; }
public Bundle ChildBundle { get; set; }
}
public class BundleMetaDataDto
{
[DataMember(IsRequired = true, Order = 15)]
public BundleMetaDataDto ChildBundle { get; set; }
}
Map:
cfg.CreateMap<Bundle, BundleMetaDataDto>()
.ForMember(dest => dest.ChildBundle, opt => opt.MapFrom(src => src.ChildBundle))
Query:
var bundles = context.Bundles
.Where(bundle => bundle.ChildBundle != null)
.ProjectTo<BundleMetaDataDto>(EntityConverter.MapperConfiguration)
.ToArray();
Thanks to #LucianBargaoanu I got it working by adding:
cfg.Advanced.RecursiveQueriesMaxDepth = 1;

AutoMapper: two-way, deep mapping, between domain models and viewmodels

I need to map two ways between a flat ViewModel and a deep structured Domain model. This will be a common scenario in our solution.
My models are:
public class Client
{
...
public NotificationSettings NotificationSettings { get; set; }
public ContactDetails ContactDetails { get; set; }
...
}
public class NotificationSettings
{
...
public bool ReceiveActivityEmails { get; set; }
public bool ReceiveActivitySms { get; set; }
...
}
public class ContactDetails
{
...
public string Email { get; set }
public string MobileNumber { get; set; }
...
}
public class ClientNotificationOptionsViewModel
{
public string Email { get; set }
public string MobileNumber { get; set; }
public bool ReceiveActivityEmails { get; set; }
public bool ReceiveActivitySms { get; set; }
}
Mapping code:
Mapper.CreateMap<Client, ClientNotificationOptionsViewModel>()
.ForMember(x => x.ReceiveActivityEmails, opt => opt.MapFrom(x => x.NotificationSettings.ReceiveActivityEmails))
.ForMember(x => x.ReceiveActivitySms, opt => opt.MapFrom(x => x.NotificationSettings.ReceiveActivitySms))
.ForMember(x => x.Email, opt => opt.MapFrom(x => x.ContactDetails.Email))
.ForMember(x => x.MobileNumber, opt => opt.MapFrom(x => x.ContactDetails.MobileNumber));
// Have to use AfterMap because ForMember(x => x.NotificationSettings.ReceiveActivityEmail) generates "expression must resolve to top-level member" error
Mapper.CreateMap<ClientNotificationOptionsViewModel, Client>()
.IgnoreUnmapped()
.AfterMap((from, to) =>
{
to.NotificationSettings.ReceiveActivityEmail = from.ReceiveActivityEmail;
to.NotificationSettings.ReceiveActivitySms = from.ReceiveActivitySms;
to.ContactDetails.Email = from.Email;
to.ContactDetails.MobileNumber = from.MobileNumber;
});
...
// Hack as ForAllMembers() returns void instead of fluent API syntax
public static IMappingExpression<TSource, TDest> IgnoreUnmapped<TSource, TDest>(this IMappingExpression<TSource, TDest> expression)
{
expression.ForAllMembers(opt => opt.Ignore());
return expression;
}
I dislike it because:
1) It is cumbersome
2) The second mapping pretty much dismantles Automapper's functionality and implements the work manually - the only advantage of it is consistency of referencing Automapper throughout the code
Can anyone suggest:
a) A better way to use Automapper for deep properties?
b) A better way to perform two-way mapping like this?
c) Advice on whether I should bother using Automapper in this scenario? Is there a compelling reason not to revert to the simpler approach of coding it up manually? eg.:
void MapManually(Client client, ClientNotificationOptionsViewModel viewModel)
{
viewModel.Email = client.ContactDetails.Email;
// etc
}
void MapManually(ClientNotificationOptionsViewModel viewModel, Client client)
{
client.ContactDetails.Email = viewModel.Email;
// etc
}
-Brendan
P.S. restructuring domain models is not the solution.
P.P.S It would be possible to clean up the above code through extension methods & some funky reflection to set deep properties... but I'd rather use automapper features if possible.
This can be done also in this way:
Mapper.CreateMap<ClientNotificationOptionsViewModel, Client>()
.ForMember(x => x.NotificationSettings, opt => opt.MapFrom(x => new NotificationSettings() { ReceiveActivityEmails = x.ReceiveActivityEmails, ReceiveActivitySms = x.ReceiveActivitySms}))
.ForMember(x => x.ContactDetails, opt => opt.MapFrom(x => new ContactDetails() { Email = x.Email, MobileNumber = x.MobileNumber }));
But is not much different than your solution.
Also, you can do it by creating a map from your model to your inner classes:
Mapper.CreateMap<ClientNotificationOptionsViewModel, ContactDetails>();
Mapper.CreateMap<ClientNotificationOptionsViewModel, NotificationSettings>();
Mapper.CreateMap<ClientNotificationOptionsViewModel, Client>()
.ForMember(x => x.NotificationSettings, opt => opt.MapFrom(x => x))
.ForMember(x => x.ContactDetails, opt => opt.MapFrom(x => x));
You don't need to specify ForMember in the new mappings because the properties has the same name in both classes.
In the end I found AutoMapper was unsuited to my scenario.
Instead I built a custom utility to provide bidirectional mapping & deep property mapping allowing configuration as follows. Given the scope of our project I believe this is justified.
BiMapper.CreateProfile<Client, ClientNotificationsViewModel>()
.Map(x => x.NotificationSettings.ReceiveActivityEmail, x => x.ReceiveActivityEmail)
.Map(x => x.NotificationSettings.ReceiveActivitySms, x => x.ReceiveActivitySms)
.Map(x => x.ContactDetails.Email, x => x.Email)
.Map(x => x.ContactDetails.MobileNumber, x => x.MobileNumber);
BiMapper.PerformMap(client, viewModel);
BiMapper.PerformMap(viewModel, client);
Apologies I cannot share the implementation as it's commercial work. However I hope it helps others to know that it isn't impossible to build your own, and can offer advantages over AutoMapper or doing it manually.

Load properties of a type without other properties of a type?

Is it possible to eager load one property of a type without loading all it's properties?
Real life situation:
I have two entities, MediaLink and MediaItem.
My media item looks like this:
public partial class MediaItem
{
public MediaItem()
{
this.MediaLink = new HashSet<MediaLink>();
}
public int Id { get; set; }
public byte[] Content { get; set; }
public int Length { get; set; }
public string MimeType { get; set; }
public virtual ICollection<MediaLink> MediaLink { get; set; }
}
When writing a query, I need to access the MimeType of the MediaItem from the MediaLink entity.
Yet when I use the navigation property to access the MimeType, the query also returns the Content which on a video could be >10MB and cripples the performance.
I suspect this is not possible, but is there any way I can return the MimeType property of a MediaItem without it's content via EF?
EDIT: Thanks for the comments, I should probably add that I think I could do this with a standard linq query by projecting the parent MediaLink and the property I want from MediaItem into a new type but I'm needing to do this inside and AutoMapper mapping.
At the moment, the below mapping requires me to go into the MediaItem via the navigation property:
Mapper.CreateMap<MediaLink, MediaModel>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.MediaTypeId))
.ForMember(dest => dest.MimeType, opt => opt.MapFrom(src => src.MediaItem.MimeType));
What I really want to do is something like this:
Mapper.CreateMap<MediaLink, MediaModel>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.MediaTypeId))
.ForMember(dest => dest.MimeType, opt => opt.MapFrom(src => (from mi in src.MediaItem select mi.MimeType)));
And hope the projection would return the whole MediaItem entity.

How to map Integer to String with AutoMapper 3 and Entity Framework

I am trying to use AutoMapper 3 to project a class with an Integer property to another class with a String property.
When the query is executed then I get the following exception:
System.NotSupportedException: LINQ to Entities does not recognize the method 'System.String ToString()' method, and this method cannot be translated into a store expression.
Here are the relevant parts of the code:
public partial class Lookup
{
public int LookupId { get; set; }
public int LookupTypeId { get; set; }
public string Value { get; set; }
public int SequencialOrder { get; set; }
public virtual LookupType LookupType { get; set; }
}
public class LookupProfile : Profile
{
protected override void Configure()
{
CreateMap<Lookup, SelectListItem>()
.ForMember(dest => dest.Value, opt => opt.MapFrom(src => src.LookupId.ToString()))
.ForMember(dest => dest.Text, opt => opt.MapFrom(src => src.Value));
}
}
And the query looks like:
Provinces = _db.Lookups.Project().To<SelectListItem>().ToList()
Question:
Is there a way I could configure the LookupProfile to do the proper mapping and still work inside Linq To Entities?
Or is there another way I could make the projection work with Linq to Entities?
The solution was to use the SqlFunctions.StringConvert function.
Here is the modified profile code that made everything work:
public class LookupProfile : Profile
{
protected override void Configure()
{
CreateMap<Lookup, SelectListItem>()
.ForMember(dest => dest.Value, opt => opt.MapFrom(src => SqlFunctions.StringConvert((double)src.LookupId)))
.ForMember(dest => dest.Text, opt => opt.MapFrom(src => src.Value));
}
}
I'll leave this answer here in case anyone else stumbles upon the same issue I had.
One problem with the current accepted answer is that if you're on an ASP.NET MVC project using client-side validation through helpers, you'll get a validation error for the ID field (if it's a number): The field [field] must be a number. That happens because the result from SqlFunctions.StringConvert returns a string with several leading spaces, so the unobtrusive validator doesn't see it as a number.
The way I solved this issue on my own was to create a generic SelectListItem<T> class that inherits from SelectListItem, hides the original Value property and implements its own Value setter:
public class SelectListItem<T> : SelectListItem
{
public new T Value {
set {
base.Value = value.ToString();
}
// Kind of a hack that I had to add
// otherwise the code won't compile
get {
return default(T);
}
}
}
Then on the Automapper profile I would map the items like so:
public class LookupProfile : Profile
{
protected override void Configure()
{
//Use whatever datatype is appropriate: decimal, int, short, etc
CreateMap<Lookup, SelectListItem<int>>()
.ForMember(dest => dest.Value, opt => opt.MapFrom(src => src.LookupId))
.ForMember(dest => dest.Text, opt => opt.MapFrom(src => src.Value));
}
}
And finally on the Service layer, I would map the entities to the generic class and return an IEnumerable<SelectListItem>.
public IEnumerable<SelectListItem> GetList() {
return _db.Lookups.Project().To<SelectListItem<int>>().ToList();
}
This way you'll get the right value for the Value property without trailing spaces.

Categories

Resources