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.
Related
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;
I have two classes and map one to other with Automapper. For instance:
public class Source
{
// IdName is a simple class containing two fields: Id (int) and Name (string)
public IdName Type { get; set; }
public int TypeId {get; set; }
// another members
}
public class Destination
{
// IdNameDest is a simple class such as IdName
public IdNameDest Type { get; set; }
// another members
}
Then I use Automapper to map Source to Destination:
cfg.CreateMap<Source, Destination>();
It works properly but sometimes member Type in class Source becomes null. In these cases I would like to map member Type in class Destination from TypeId property. That's what I want in a nutshel:
if Source.Type != null
then map Destination.Type from it
else map it as
Destination.Type = new IdNameDest { Id = Source.Id }
Is it possible with AutoMapper?
You can use the .ForMember() method while declaring the mapping.
Like so :
cfg.CreateMap<Source, Destination>()
.ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.Type != null ? src.Type : new IdNameDest { Id = src.Id }));
While LeeeonTMs answer works fine AutoMapper provides a specialised mechanism to substitute null values. It "allows you to supply an alternate value for a destination member if the source value is null anywhere along the member chain" (taken from the AutoMapper manual).
Example:
cfg.CreateMap<Source, Destination>()
.ForMember(dest => dest.Value, opt => opt.NullSubstitute(new IdNameDest { Id = src.Id }));
With C# 6.0, the null-coalescing operator can be used.
cfg.CreateMap<Source, Destination>()
.ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.Type ?? new IdNameDest { Id = src.Id }));
I managed to resolve it with Mapping Resolvers
public class SomeResolver : IValueResolver<Soruce, Dest, Guid>
{
public Guid Resolve(Source source, Dest destination, Guid, destMember, ResolutionContext context)
{
destination.Value= source.Value!=null ? source.Value:0;
return destination.MainGuid = Guid.NewGuid();
}
}
and then on mapping configuraiton
CreateMap<BioTimeEmployeeSummaryDTO, BioTimeEmployeeAttendanceSummary>()
.ForMember(dest => dest.MainGuid, opt => opt.MapFrom<YourResolverClass>())
.ReverseMap();
I'm having some trouble getting this to work correctly. I have two classes:
public class TestClassA
{
public int? NullableIntProperty { get; set; }
}
public class TestClassB
{
public int NotNullableIntProperty { get; set; }
}
I then set up the following mappings:
cfg.CreateMap<TestClassA, TestClassB>()
.ForMember(dest => dest.NotNullableIntProperty,
opt => opt.MapFrom(src => src.NullableIntProperty));
cfg.CreateMap<TestClassA, TestClassA>()
.ForMember(dest => dest.NullableIntProperty,
opt => opt.MapFrom(src => src.NullableIntProperty));
cfg.CreateMap<TestClassB, TestClassA>()
.ForMember(dest => dest.NullableIntProperty,
opt => opt.MapFrom(src => src.NotNullableIntProperty));
cfg.CreateMap<TestClassB, TestClassB>()
.ForMember(dest => dest.NotNullableIntProperty,
opt => opt.MapFrom(src => src.NotNullableIntProperty));
I now have four mappings set up, and will test the following scenarios:
int? => int
int => int?
int => int
int? => int?
In a test class, I then use the mappings like this:
var testQueryableDest = testQueryableSrc.ProjectTo<...>(_mapper.ConfigurationProvider);
The only projection I would expect not to work at this stage would be TestClassA => TestClassB, since I can see how AutoMapper may not know what to do with the int? in cases where the value is null. Sure enough, that's exactly the case. So I set up a mapping for int? => int like so:
cfg.CreateMap<int?, int>()
.ProjectUsing(src => src ?? default(int));
This is where things become strange. As soon as I add this mapping, the mapping from TestClassB => TestClassB fails to even create. It gives this error message:
Expression of type 'System.Int32' cannot be used for assignment to type 'System.Nullable`1[System.Int32]'
I find this message incredibly strange as TestClassB does not have an int? on it at all. So what's going on here? I feel like I must be misunderstanding something about how AutoMapper needs these projections to be handled. I realise the various bits of code may be tricky to piece together so here's the entire test class for reference:
[TestClass]
public class BasicTests
{
private readonly IMapper _mapper;
public BasicTests()
{
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<int?, int>()
.ProjectUsing(src => src ?? default(int));
cfg.CreateMap<TestClassA, TestClassB>()
.ForMember(dest => dest.IntProperty, opt => opt.MapFrom(src => src.NullableIntProperty));
cfg.CreateMap<TestClassA, TestClassA>()
.ForMember(dest => dest.NullableIntProperty, opt => opt.MapFrom(src => src.NullableIntProperty));
cfg.CreateMap<TestClassB, TestClassA>()
.ForMember(dest => dest.NullableIntProperty, opt => opt.MapFrom(src => src.IntProperty));
cfg.CreateMap<TestClassB, TestClassB>()
.ForMember(dest => dest.IntProperty, opt => opt.MapFrom(src => src.IntProperty));
});
_mapper = new Mapper(config);
}
[TestMethod]
public void CanMapNullableIntToInt()
{
var testQueryableSource = new List<TestClassA>
{
new TestClassA
{
NullableIntProperty = null
}
}.AsQueryable();
var testQueryableDestination = testQueryableSource.ProjectTo<TestClassB>(_mapper.ConfigurationProvider);
}
[TestMethod]
public void CanMapNullableIntToNullableInt()
{
var testQueryableSource = new List<TestClassA>
{
new TestClassA
{
NullableIntProperty = null
}
}.AsQueryable();
var testQueryableDestination = testQueryableSource.ProjectTo<TestClassA>(_mapper.ConfigurationProvider);
}
[TestMethod]
public void CanMapIntToNullableInt()
{
var testQueryableSource = new List<TestClassB>
{
new TestClassB
{
IntProperty = 0
}
}.AsQueryable();
var testQueryableDestination = testQueryableSource.ProjectTo<TestClassA>(_mapper.ConfigurationProvider);
}
[TestMethod]
public void CanMapIntToInt()
{
var testQueryableSource = new List<TestClassB>
{
new TestClassB
{
IntProperty = 0
}
}.AsQueryable();
var testQueryableDestination = testQueryableSource.ProjectTo<TestClassB>(_mapper.ConfigurationProvider);
}
}
I’ve found the shortest way to reproduce this situation is the following:
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<int?, int>().ProjectUsing(x => x ?? default(int));
cfg.CreateMap<TestClassA, TestClassA>()
.ForMember(a => a.NullableIntPropety, o => o.MapFrom(a => a.NullableIntProperty));
}
It seems to me that AutoMapper is attempting to use the int? => int mapper here although a more obvious identity-based mapping would be to use here.
Since every int is also a valid int?, AutoMapper attempts to use the int? => int mapper here and assign the result to the int? member. But it seems that under the hood something does not correctly work when resolving just that assignment, hence that exception.
What seems to fix it is to add another mapping, an identity mapping for int? => int?:
cfg.CreateMap<int?, int?>().ProjectUsing(x => x);
Then, this mapping is being used instead and no exception occurs (and the mapping also properly works—with all of your examples).
This problem seems to exist on the current AutoMapper 5.1.x release (current is 5.1.1). The good news is, that it has already been fixed. If you try the current 5.2 alpha from the myget feed, then the code works fine without any issues.
Since the 5.1.1 release, the code base has seen quite a few contributions with multiple fixes on nullable mapping (e.g. this and this pull request). I assume that one of those changes has fixed this problem.
Most likely, it was pull request #1672 which just meant to remove unneeded code but apparently also fixed issue 1664 which was about AutoMapper apparently prioritizing nullable source mappings over non-nullable sources even if a non-nullable source was being mapped. And that sounds very much like this very problem you have experienced.
So, for now, you can add above workaround to map the type to itself or use an alpha release, while we wait for 5.2 to be released.
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.
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.