I'm updating an old .NET Framework application to the latest supported Automapper nuget (from v3.3.1 to v10.0.0). The original code extends automapper in horrendous ways, which I have been able to convert to use built in methods, except this one:
// Ignores all destination properties which would otherwise be mapped by convention.
public static IMappingExpression<TSource, TDestination> IgnoreUnMapped<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
{
TypeMap map = Mapper.FindTypeMapFor<TSource, TDestination>();
foreach (var property in map.GetCustomPropertyMaps()
.Where(p => p.CustomExpression == null)
.Select(p => p.DestinationProperty.Name))
{
expression.ForMember(property, opt => opt.Ignore());
}
return expression;
}
I found this page , that gives a workaround for v4.2, but that no longer works for v10.
This requires a cast, but this is the only option that Automapper authors left for people that actually have to maintain software:
var typeMapConfiguration = (TypeMapConfiguration)expression;
foreach (var property in typeMapConfiguration.TypeMap.GetUnmappedPropertyNames())
{
expression.ForMember(property, opt => opt.Ignore());
}
Related
I have the following query:
var query = _context.QuestOrders.Include(a => a.Driver).ThenInclude(i => i.DlStateProvince)
.Where(p => carrierIds.Contains(p.Driver.CarrierId))
....
;
and then try to call the following:
var queryDto = query.AsNoTracking().ProjectTo<DcReportDonorResultDto>(_mapperConfiguration);
var reports = new PagedList<DcReportDonorResultDto>(queryDto, pageIndex, pageSize);
where DcReportDonorResultDto has a property:
public string PrimaryId { get; set; }
which mapped the following:
CreateMap<QuestOrder, DcReportDonorResultDto>()
.ForMember(destinationMember => destinationMember.PrimaryId, opt => opt.MapFrom(src => src.Driver.PrimaryId))
and PrimaryId is defined in QuestOrder as:
public string PrimaryId
{
get
{
if (!string.IsNullOrWhiteSpace(DlNumber) && DlStateProvinceId.HasValue)
return DlStateProvince.Abbreviation + DlNumber.Replace("-", "");
else
return string.Empty;
}
}
I received the following error:
System.InvalidOperationException: 'Error generated for warning
'Microsoft.EntityFrameworkCore.Infrastructure.DetachedLazyLoadingWarning:
An attempt was made to lazy-load navigation property 'DlStateProvince'
on detached entity of type ''. Lazy-loading is not supported for
detached entities or entities that are loaded with 'AsNoTracking()'.'.
This exception can be suppressed or logged by passing event ID
'CoreEventId.DetachedLazyLoadingWarning' to the 'ConfigureWarnings'
method in 'DbContext.OnConfiguring' or 'AddDbContext'.'
How to solve this problem?
The problem is caused by the computed QuestOrder.PrimaryId property.
When used in LINQ to Entities queries, such properties cannot be translated to SQL and require client evaluation. And even when supported, client evaluation does not play well when accessing navigation properties inside - both eager or lazy loading do not function properly and lead to either runtime exceptions or wrong return value.
So the best is to make them translatable, which requires dealing with translatable expressions.
In all the cases, start by converting the computed property body from block to conditional operator (to make it translatable):
public string PrimaryId =>
!string.IsNullOrWhiteSpace(this.DlNumber) && this.DlStateProvinceId.HasValue) ?
this.DlStateProvince.Abbreviation + this.DlNumber.Replace("-", "") :
string.Empty;
Now, short turn, quick-and-dirty solution is to extract the actual expression for the computed property, copy/paste it in the mapping and replace this with src.Driver:
.ForMember(dst => dst.PrimaryId, opt => opt.MapFrom(src =>
//src.Driver.PrimaryId
!string.IsNullOrWhiteSpace(src.Driver.DlNumber) && src.Driver.DlStateProvinceId.HasValue) ?
src.Driver.DlStateProvince.Abbreviation + src.Driver.DlNumber.Replace("-", "") :
string.Empty
))
Long term, or if you have many properties like this, or you need to use it in other mappings/queries, or just because of the code duplication, this is not a good solution. You need a way to replace the computed property accessor inside query expression tree with the corresponding expression extracted from the body.
Neither C#, nor BCL or EF Core help in that regard. Several 3rd party packages are trying to address this problem to some sort of degree - LinqKit, NeinLinq etc., but there is a little not very well known gem called DelegateDecompiler which does that with minimum code changes.
All you need is to install the DelegateDecompiler or DelegateDecompiler.EntityFrameworkCore package, mark your computed properties with [Computed] attribute
[Computed] // <--
public string PrimaryId =>
!string.IsNullOrWhiteSpace(this.DlNumber) && this.DlStateProvinceId.HasValue) ?
this.DlStateProvince.Abbreviation + this.DlNumber.Replace("-", "") :
string.Empty;
and then call Decompile (or DecompileAsync) on the top level queryable
var queryDto = query.AsNoTracking()
.ProjectTo<DcReportDonorResultDto>(_mapperConfiguration)
.Decompile(); // <--
No special mappings are needed for AutoMapper, e.g. you can keep the usual
.ForMember(dst => dst.PrimaryId, opt => opt.MapFrom(src => src.Driver.PrimaryId)
For AutoMapper projection queries (produced with ProjectTo) you can even eliminate the need for calling Decompile / DecompileAsync by providing the following little `"bridge" between the two libraries:
namespace AutoMapper
{
using DelegateDecompiler;
using QueryableExtensions;
public static class AutoMapperExtensions
{
public static IMapperConfigurationExpression UseDecompiler(this IMapperConfigurationExpression config)
{
var resultConverters = config.Advanced.QueryableResultConverters;
for (int i = 0; i < resultConverters.Count; i++)
{
if (!(resultConverters[i] is ExpressionResultDecompiler))
resultConverters[i] = new ExpressionResultDecompiler(resultConverters[i]);
}
return config;
}
class ExpressionResultDecompiler : IExpressionResultConverter
{
IExpressionResultConverter baseConverter;
public ExpressionResultDecompiler(IExpressionResultConverter baseConverter) => this.baseConverter = baseConverter;
public bool CanGetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, PropertyMap propertyMap) => baseConverter.CanGetExpressionResolutionResult(expressionResolutionResult, propertyMap);
public bool CanGetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, ConstructorParameterMap propertyMap) => baseConverter.CanGetExpressionResolutionResult(expressionResolutionResult, propertyMap);
public ExpressionResolutionResult GetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, PropertyMap propertyMap, LetPropertyMaps letPropertyMaps) => Decompile(baseConverter.GetExpressionResolutionResult(expressionResolutionResult, propertyMap, letPropertyMaps));
public ExpressionResolutionResult GetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, ConstructorParameterMap propertyMap) => Decompile(baseConverter.GetExpressionResolutionResult(expressionResolutionResult, propertyMap));
static ExpressionResolutionResult Decompile(ExpressionResolutionResult result)
{
var decompiled = DecompileExpressionVisitor.Decompile(result.ResolutionExpression);
if (decompiled != result.ResolutionExpression)
result = new ExpressionResolutionResult(decompiled, result.Type);
return result;
}
}
}
}
and just call UseDecompiler() during AutoMapper initialization, for instance
var mapperConfig = new MapperConfiguration(config =>
{
config.UseDecompiler(); // <--
// the rest (add profiles, create maps etc.) ...
});
I've set my mapping up as follows:
CreateMap<SourceClass, DestinationClass>().ForMember(destinationMember => destinationMember.Provider,
memberOptions => memberOptions.MapFrom(src => src.Providers.FirstOrDefault()));
Where I'm mapping from a List in my SourceClass to a string in my destination class.
My question is, how can I handle the case where "Providers" is null?
I've tried using:
src?.Providers?.FirstOrDefault()
but I get an error saying I can't use null propagators in a lambda.
I've been reading up on Automapper and am still unsure if AM automatically handles the null case or not. I tried to build the expression tree, but was not able to see any information that provided additional informations.
If it helps, I'm using automapper v 6.1.1.
You could try using a ValueConverter with AutoMapper. That might look something like this
public class ListFormatter : IValueConverter<string, List<string>>
{
public List<string> Convert(string source)
{
if (source != null)
{
return new List<string> { source };
}
return new List<string>();
}
}
And then you can use it like this
CreateMap<SourceClass, DestinationClass>()
.ForMember(destinationMember => destinationMember.Provider,
memberOptions => memberOptions.ConvertUsing(new ListFormatter()));
This would allow you to change your value converter in the future if you need to switch logic or do something more complex.
Edit
Since you are using an older version, you could use a private/static/extension method to do the same thing. So something like
List<string> ConvertStringToList(string source)
{
if (source != null)
{
return new List<string> { source };
}
return new List<string>();
}
and then call it like so
CreateMap<SourceClass, DestinationClass>()
.ForMember(destinationMember => destinationMember.Provider,
memberOptions => memberOptions.MapFrom(src => ConvertStringToList(src.Provider)));
I generally prefer this to doing something inline as things get more complex, for the sake of readability
Try to use NullSubstitution option from AutoMapper
you can read here
Is there a way to pass the properties I want to retrieve from the DB in a Select dynamically, I don't know the properties I need beforehand and I don't want to write the conditions in my repository.
I don't want to retrieve all the fields at once, just the ones I need based on some conditions.
For example:
public class Student
{
public string Property1 {get; set;}
public string Property2 {get; set;}
//other properties here
[NotMapped]
public string Selected
{
if(condition)
return Property1;
else
return Property2;
}
}
and in the service layer I have
query.Select(s => new StudentViewModel
{
Value = s.Selected; //this one will get the property we want based on a condition
//other stuff here
//OtherValue = s.OtherProperty
}
).FirstOrDefault();
Selector
An easy but ugly way is to use a Selector:
query.Select(Selector()).FirstOrDefault();
And the Selector can look like this:
private static Expression<Func<Student, StudentViewModel>> Selector()
{
if (Condition())
return x => new StudentViewModel
{
Name = x.Property1,
OtherName = x.OtherName
};
return x => new StudentViewModel
{
Name = x.Property2,
OtherName = x.OtherName
};
}
As you can see the obviously downside here is that you need to copy/paste all other selected properties. That is why its ugly.
AutoMapper
Configs
You can use AutoMapper with different configurations. First you need to define a standard mapping for all properties that don't need to be dynamic.
public static void AddStandardStudentMap(this IMappingExpression<Student, StudentViewModel> map)
{
map.ForMember(dest => dest.OtherName, opt => opt.MapFrom(src => src.OtherProperty))
.ForMember(dest => dest.OtherName2, opt => opt.MapFrom(src => src.OtherProperty2));
// you can concat .ForMember() for each property you need.
}
Next, you need to define the different configs and add the AddStandardStudentMap method to each invidual mapping.
var config1 = new MapperConfiguration(cfg =>
cfg.CreateMap<Student, StudentViewModel>()
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Property1))
.AddStandardStudentMap()
);
var config2 = new MapperConfiguration(cfg =>
cfg.CreateMap<Student, StudentViewModel>()
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Property2))
.AddStandardStudentMap()
);
After this, just use your conditions to decide which config do you need
IConfigurationProvider provider;
if(Condition())
provider = config1;
else
provider = config2;
And then instead of .Select() use:
query.ProjectTo<StudentViewModel>(provider).FirstOrDefault();
As we can see this solution is still ugly and has a lot of overhead but its needed in some cases, thats why i stated it here.
Expression
This is a bit similar to the Configs but brings you more flexibility and less writing effort.
First create a config but this time with a Selector
var config = new MapperConfiguration(cfg =>
cfg.CreateMap<Student, StudentViewModel>()
.ForMember(dest => dest.OtherName, opt => opt.MapFrom(src => src.OtherName))
.ForMember(dest => dest.OtherName2, opt => opt.MapFrom(src => src.OtherName2))
// and so on. Map all your properties that are not dynamically.
// and then the Selector
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => Selector()))
);
The Selector method can look like this:
private static Expression<Func<Student, StudentViewModel>> Selector()
{
if(Condition())
return src => src.Property1;
else
return src => src.Property2;
}
And then just use it like the configs solution but without selecting a particular config:
query.ProjectTo<StudentViewModel>(config).FirstOrDefault();
Conclusion
I know this is a lot input and there are even more possibilities to achieve the behaviour that you want, with or without AutoMapper. But i would suggest you (grounded on the information you gave us) to use AutoMapper with Expressions. It should give the flexibility you need and is extensible for further requirements.
I have a simple update function:
public void Update(Users user)
{
tblUserData userData = _context.tblUserDatas.Where(u => u.IDUSER == user.IDUSER).FirstOrDefault();
if (userData != null)
{
Mapper.CreateMap<Users, tblUserData>();
userData = Mapper.Map<Users, tblUserData>(user);
_context.SaveChanges()
}
}
userData is an EF entity, and it's Entity Key property gets nulled out because, I believe, it exists in the destination object, but not in the source object, so it gets mapped over with its default value (for the Entity Key, that's null)
So, my question is can Automapper be configured to only attempt to map the properties that exist in both the source and destination objects? I'd like things like the Entity Key and navigation properties to be skipped over.
You can explicitly tell AutoMapper to Ignore certain properties if required like this:
Mapper.CreateMap<Users, tblUserData>()
.ForMember(dest => dest.Id, opt => opt.Ignore());
which will mean the Id column in the destination object will ALWAYS be left untouched.
You can use the Condition option to specify whether or not a mapping is applied depending on the result of a logical condition, like this:
Mapper.CreateMap<Users, tblUserData>()
.ForMember(dest => dest.Id, opt => opt.Condition(src=>src.Id.HasValue));
or
Mapper.CreateMap<Users, tblUserData>()
.ForMember(dest => dest.Id, opt => opt.Condition(src=>src.Id != null));
depending on your specific requirements.
Instead of
userData = _mapper.Map<Users, tblUserData>(user);
use
_mapper.Map(user, userData);
The former creates a new userData object with only the properties available in user object. The later overwrites the existing userData object's properties with properties present in user object, while retaining the rest.
You can tell AutoMapper to ignore fields that you do not want mapped like this:
userData = Mapper.Map<Users, tblUserData>(user).ForMember(m => m.EntityKey, opt => opt.Ignore());
You can override this behavior by making a small extension method to ignore all properties that do not exist on the target type.
public static IMappingExpression<TSource, TDestination> IgnoreAllNonExisting<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
{
var sourceType = typeof(TSource);
var destinationType = typeof(TDestination);
var existingMaps = Mapper.GetAllTypeMaps().First(x => x.SourceType.Equals(sourceType)
&& x.DestinationType.Equals(destinationType));
foreach (var property in existingMaps.GetUnmappedPropertyNames())
{
expression.ForMember(property, opt => opt.Ignore());
}
return expression;
}
Then it is possible to do the mapping as follows:
Mapper.CreateMap<SourceType, DestinationType>().IgnoreAllNonExisting();
It is also possible to customize this method to your needs, by specifically ignoring properties which have a protected or private setter, for example.
I have been flattening domain objects into DTOs as shown in the example below:
public class Root
{
public string AParentProperty { get; set; }
public Nested TheNestedClass { get; set; }
}
public class Nested
{
public string ANestedProperty { get; set; }
}
public class Flattened
{
public string AParentProperty { get; set; }
public string ANestedProperty { get; set; }
}
// I put the equivalent of the following in a profile, configured at application start
// as suggested by others:
Mapper.CreateMap<Root, Flattened>()
.ForMember
(
dest => dest.ANestedProperty
, opt => opt.MapFrom(src => src.TheNestedClass.ANestedProperty)
);
// This is in my controller:
Flattened myFlattened = Mapper.Map<Root, Flattened>(myRoot);
I have looked at a number of examples, and so far this seems to be the way to flatten a nested hierarchy. If the child object has a number of properties, however, this approach doesn't save much coding.
I found this example:
http://consultingblogs.emc.com/owainwragg/archive/2010/12/22/automapper-mapping-from-multiple-objects.aspx
but it requires instances of the mapped objects, required by the Map() function, which won't work with a profile as I understand it.
I am new to AutoMapper, so I would like to know if there is a better way to do this.
I much prefer avoiding the older Static methods and do it like this.
Place our mapping definitions into a Profile. We map the Root first, and then apply the mappings of the Nested afterwards. Note the use of the Context.
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<Root, Flattened>()
.AfterMap((src, dest, context) => context.Mapper.Map(src.TheNestedClass, dest));
CreateMap<Nested, Flattened>();
}
}
The advantage of defining both the mapping from Root to Flattened and Nested to Flatterned is that you retain full control over the mapping of the properties, such as if the destination property name is different or you want to apply a transformation etc.
An XUnit test:
[Fact]
public void Mapping_root_to_flattened_should_include_nested_properties()
{
// ARRANGE
var myRoot = new Root
{
AParentProperty = "my AParentProperty",
TheNestedClass = new Nested
{
ANestedProperty = "my ANestedProperty"
}
};
// Manually create the mapper using the Profile
var mapper = new MapperConfiguration(cfg => cfg.AddProfile(new MappingProfile())).CreateMapper();
// ACT
var myFlattened = mapper.Map<Root, Flattened>(myRoot);
// ASSERT
Assert.Equal(myRoot.AParentProperty, myFlattened.AParentProperty);
Assert.Equal(myRoot.TheNestedClass.ANestedProperty, myFlattened.ANestedProperty);
}
By adding AutoMapper's serviceCollection.AddAutoMapper() from the AutoMapper.Extensions.Microsoft.DependencyInjection nuget package to your start up, the Profile will be picked up automatically, and you can simply inject IMapper into wherever you are applying the mapping.
In the latest version of AutoMapper, there's a naming convention you can use to avoid multiple .ForMember statements.
In your example, if you update your Flattened class to be:
public class Flattened
{
public string AParentProperty { get; set; }
public string TheNestedClassANestedProperty { get; set; }
}
You can avoid the use of the ForMember statement:
Mapper.CreateMap<Root, Flattened>();
Automapper will (by convention) map Root.TheNestedClass.ANestedProperty to Flattened.TheNestedClassANestedProperty in this case. It looks less ugly when you're using real class names, honest!
2 more possible solutions:
Mapper.CreateMap<Nested, Flattened>()
.ForMember(s=>s.AParentProperty, o=>o.Ignore());
Mapper.CreateMap<Root, Flattened>()
.ForMember(d => d.ANestedProperty, o => o.MapFrom(s => s.TheNestedClass));
An alternative approach would be the below, but it would not pass the Mapper.AssertConfigurationIsValid().
Mapper.CreateMap<Nested, Flattened>()
//.ForMember map your properties here
Mapper.CreateMap<Root, Flattened>()
//.ForMember... map you properties here
.AfterMap((s, d) => Mapper.Map(s.TheNestedClass, d));
Not sure if this adds value to the previous solutions, but you could do it as a two-step mapping. Be careful to map in correct order if there are naming conflicts between the parent and child (last wins).
Mapper.CreateMap<Root, Flattened>();
Mapper.CreateMap<Nested, Flattened>();
var flattened = new Flattened();
Mapper.Map(root, flattened);
Mapper.Map(root.TheNestedClass, flattened);
To improve upon another answer, specify MemberList.Source for both mappings and set the nested property to be ignored. Validation then passes OK.
Mapper.Initialize(cfg =>
{
cfg.CreateMap<SrcNested, DestFlat>(MemberList.Source);
cfg.CreateMap<SrcRoot, DestFlat>(MemberList.Source)
.ForSourceMember(s => s.Nested, x => x.Ignore())
.AfterMap((s, d) => Mapper.Map(s.Nested, d));
});
Mapper.AssertConfigurationIsValid();
var dest = Mapper.Map<SrcRoot, DestFlat>(src);
I wrote extension method to solve similar problem:
public static IMappingExpression<TSource, TDestination> FlattenNested<TSource, TNestedSource, TDestination>(
this IMappingExpression<TSource, TDestination> expression,
Expression<Func<TSource, TNestedSource>> nestedSelector,
IMappingExpression<TNestedSource, TDestination> nestedMappingExpression)
{
var dstProperties = typeof(TDestination).GetProperties().Select(p => p.Name);
var flattenedMappings = nestedMappingExpression.TypeMap.GetPropertyMaps()
.Where(pm => pm.IsMapped() && !pm.IsIgnored())
.ToDictionary(pm => pm.DestinationProperty.Name,
pm => Expression.Lambda(
Expression.MakeMemberAccess(nestedSelector.Body, pm.SourceMember),
nestedSelector.Parameters[0]));
foreach (var property in dstProperties)
{
if (!flattenedMappings.ContainsKey(property))
continue;
expression.ForMember(property, opt => opt.MapFrom((dynamic)flattenedMappings[property]));
}
return expression;
}
So in your case it can be used like this:
var nestedMap = Mapper.CreateMap<Nested, Flattened>()
.IgnoreAllNonExisting();
Mapper.CreateMap<Root, Flattened>()
.FlattenNested(s => s.TheNestedClass, nestedMap);
IgnoreAllNonExisting() is from here.
Though it's not universal solution it should be enough for simple cases.
So,
You don't need to follow flattening convention in destination's properties
Mapper.AssertConfigurationIsValid() will pass
You can use this method in non-static API (a Profile) as well
I created a simple example using AutoMappers new naming convention rules to map from flattened to nested objects, hope this helps
https://dotnetfiddle.net/i55UFK