MapFrom not being called for null collection - automapper

I want to upgrade AutoMapper library in my project from v4.2.1 to the latest that still supports .NET Framework 4.5.2 (which to my knowledge is AutoMapper 7.0.1). I have code like this:
Mapper.Initialize(cfg =>
{
cfg.AllowNullCollections = true;
cfg.CreateMap<ExampleDto, Example>(MemberList.Source)
.ForMember(dest => dest.ItemsAAA, o => o.MapFrom(x => x.items.ConvertTo<TypeAAA>()))
.ForMember(dest => dest.ItemsBBB, o => o.MapFrom(x => x.items.ConvertTo<TypeBBB>()));
});
ExampleDto.items is a collection that should be split to Example.ItemsAAA and Example.ItemsBBB based on every item type. ConvertTo is an extension method, and it returns an empty collection if input parameter is null. This used to work in AutoMapper 4.2.1, but in 7.0.1 ConvertTo is never called for x.items == null. How can I make AutoMapper call this mapping even if the x.items is null?

Related

Automapper walking down relationships

I'm still trying to wrap my head around how automapper works. I have the EF Core query below, which I'd like to change to using automapper.
var query = from t in Context.Tririga.AsNoTracking()
let l = t.Building
let m = t.Owner
let o = m.Organization
where o.Active
select new MetricBadLabManagerByOrganizationDTO {
CampusName = l.CampusName,
Email = m.Email,
Name = m.Name,
OrgLevel3 = o.ThreeName,
OrgLevel4 = o.FourName,
OrgLevel5 = o.FiveName,
OrgLevel6 = o.SixName,
OrgLevel7 = o.SevenName,
Reason = m.Active == false ? "Inactive Employee" : "Invalid Employee",
SiteName = l.SiteName,
Wwid = m.Wwid
};
return await query.ToArrayAsync();
I'm not sure how to setup a mapper configuration to the DTO type because I can't just go from Tririga to MetricBadLabManagerByOrganizationDTO as it doesn't know how to go down the relationships.
Here is the Getting Started Guide from AutoMapper if you haven't gone through the documentation.
I've recently got a chance to work on a project that uses AutoMapper to translate between persistence models and domain models, and here would be how I set things up:
There are many ways to configure your mappings. I like the Profile Instances method:
using AutoMapper;
namespace Company.Product.Infrastructure.Mapping.AutoMapper
{
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<Tririga, MetricBadLabManagerByOrganizationDTO>()
.ForMember(dest => dest.CampusName,
opts => opts.MapFrom(src => src.Building.CampusName))
.ForMember(dest => dest.Email,
opts => opts.MapFrom(src => src.Owner.Email))
.ForMember(dest => dest.Name,
opts => opts.MapFrom(src => src.Owner.Name))
...
}
}
}
There is a whole section about flattening on AutoMapper documentation.
Just a note,
When you configure a source/destination type pair in AutoMapper, the configurator attempts to match properties and methods on the source type to properties on the destination type. If for any property on the destination type a property, method, or a method prefixed with “Get” does not exist on the source type, AutoMapper splits the destination member name into individual words (by PascalCase conventions).
So you might not need to define the rule for each single property you want to map, otherwise what's the point of using AutoMapper.
For example, if your MetricBadLabManagerByOrganizationDTO campus name were named as BuildingCampusName, AutoMapper would be smart enough to look for Building property on your source and see if there is a property called CampusName inside.
There are just lots of valuable information in AutoMapper documentation you can find and learn from, which is what I like!

Using string.Split() in AutoMapper issue

I have an ASP .Net core application. I am simply trying to have my AutoMapper configure to convert a string comma delimited into a list of strings as per this configuration:
configuration.CreateMap<Job, JobDto>()
.ForMember(dto => dto.Keywords, options => options.MapFrom(entity => entity.Keywords.Split(',').ToList()))
For some reason it does not get compiled and give me the following error:
An expression tree may not contain a call or invocation that uses
optional argument
I can't see why I am getting this error. I am pretty sure that I have done that in my other projects before without any such error.
As error says, Split function has an optional parameter. The full signature of it is as this (options is optional)
public string[] Split(string separator, StringSplitOptions options = StringSplitOptions.None)
As you are trying to use a function with default value inside an expression tree, it gives you the error.
To Fix it, easy, just pass on optional parameters by yourself. ( StringSplitOptions.None )
So, simply change it to this:
entity.Keywords.Split(',' , StringSplitOptions.None).ToList()
This is completely true.
Error is raised because expression tree being created is about to contain some more complex logic, like .Split(',').ToList(), which is not an accessible property or method, only top-level reflected object properties and methods are supported (like in class MemberInfo).
Property chaining, deep-calls (.obj1property.obj2property), extension methods are not supported by the expression trees, like in this .ToList() call.
My solution was like this:
// Execute a custom function to the source and/or destination types after member mapping
configuration.CreateMap<Job, JobDto>()
.AfterMap((dto,jobDto)=>jobDto.Keywords = dto.Keywords.Split(',').ToList());
I had the same problem. I do not know if it is an issue or not. Anyway, I found a workaround.
CreateMap<Category, GetCategoryRest>()
.ForMember(dest => dest.Words,
opt => opt.MapFrom(src => ToWordsList(src.Words)));
private static List<string> ToWordsList(string words)
{
return string.IsNullOrWhiteSpace(words) ? new List<string>() : words.Split(",").ToList();
}
It is guaranteed that AutoMapper has always a List. Still, I'm confused. In my Startup.cs I define that AutoMapper allows null values for list.
Mapper.Initialize(cfg => {
cfg.AllowNullCollections = true;
}
Category.Words is a string.
GetCategoryRest.Words is a List<string>
AutoMapper Version: 8.1.1,
AutoMapper.Microsoft.DependencyInjection: 6.1.1
Use .AfterMap
CreateMap<src, dto>()
.ForMember(src =>src.Categories,options=> options.Ignore())
.AfterMap((src, dto) => { dto.Categories.AddRange(src.Categories.Split(",").ToList()); })
.ReverseMap()
.ForMember(src => src.Categories, option => option.MapFrom(dto => string.Join(",", dto.Categories)));

Configuring AutoMapper to Use AutoFac

I've configured AutoMapper to, I thought, use AutoFac to create object instances by defining the following AutoFac Module:
public class AutoMapperModule : Module
{
protected override void Load( ContainerBuilder builder )
{
base.Load( builder );
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
builder.RegisterAssemblyTypes( assemblies )
.Where( t => typeof(Profile).IsAssignableFrom( t ) && !t.IsAbstract && t.IsPublic )
.As<Profile>();
builder.Register( c => new MapperConfiguration( cfg =>
{
//cfg.ConstructServicesUsing( CSContainer.Instance.Resolve );
foreach( var profile in c.Resolve<IEnumerable<Profile>>() )
{
cfg.AddProfile( profile );
}
} ) )
.AsSelf()
.AutoActivate()
.SingleInstance();
builder.Register( c => c.Resolve<MapperConfiguration>().CreateMapper( c.Resolve ) )
.As<IMapper>()
.SingleInstance();
}
}
I then created AutoMapper Profiles such as the following:
public class CommunityScannerManagerAutoMapProfile : Profile
{
public CommunityScannerManagerAutoMapProfile()
{
CreateMap<CommunityUser, CommunityUserModel>()
//.ConstructUsing(src => new CommunityUserModel(CSContainer.Instance.Resolve<IValidationService>()))
.ForMember( dest => dest.VaultKey, opt => opt.MapFrom( src => src.VaultKeyName ) )
.ReverseMap();
CreateMap<Community, CommunityModel>()
//.ConstructUsing(src=>new CommunityModel(CSContainer.Instance.Resolve<IValidationService>( )))
.ReverseMap();
CreateMap<ScannerConfiguration, CommunitiesModel>()
.ForMember( dest => dest.Communities, opt => opt.MapFrom( src => src.Communities ) )
.ReverseMap();
}
}
The destination types form a hierarchy: CommunitiesModel contains a collection of CommunityModel objects, and each CommunityModel object contains a collection of CommunityUserModel objects. The collections are mapped thru the AutoMapper Profile I created.
Each destination type -- CommunitiesModel, CommunityModel, and CommunityUserModel -- has constructor parameters which are registered with AutoFac (they themselves are also registered with AutoFac). So I thought AutoMapper would be able to resolve the instances it needed, calling upon AutoFac to create them.
But that's not the case. Unless I include those two commented out lines in the AutoMapper Profile class -- the ones starting with ConstructUsing -- I get an exception, from AutoMapper, that it can only handle classes with constructors that have no parameters, or only optional ones. Which implies AutoMapper is not using AutoFac to resolve/create those instances, contrary to what I expected.
Interestingly, AutoMapper has no problem creating instances of CommunitiesModel, the top level object, even though it, too, only has a constructor containing a parameter. So AutoMapper is using AutoFac for top level objects, but not the child objects.
Is there a global configuration I can set in AutoMapper to tell it to always use AutoFac? I tried using ConstructServicesUsing in configuring AutoMapper (the line is commented out in the code here), but it didn't seem to have any effect.
Update
Apparently, AutoMapper doesn't use DI to create objects "internally" (see #Lucian's comment).
But I noticed AutoMapper does support a ConstructUsing() clause, which allows you to tie it into a DI container like AutoMap:
CreateMap<CommunityUser, CommunityUserModel>()
.ConstructUsing(src => CSContainer.Instance.Resolve<CommunityUserModel>())
.ForMember( dest => dest.VaultKey, opt => opt.MapFrom( src => src.VaultKeyName ) )
.ReverseMap();
So a related/follow-on question is: is there a way to make this the behavior without having to add the line to every CreateMapper() call where it applies?

Automapper ResolveUsing or MapFrom

I have a mapping definition defined as
Mapper.CreateMap<Calculator, CalculatorViewModel>()
.ForMember(dest => dest.TypeIndicator, src => src.ResolveUsing(new TypeIndicatorResolver()));
Should I be using ResolveUsing or MapFrom(src => SomePrivateMethod()) ?
What is the difference between ResolveUsing and MapFrom when it comes to complex mapping.
The Resolver or Private method will go to the database and get a value.
MapFrom uses Expressions, while ResolveUsing uses a Func. MapFrom only allows redirection of properties:
ForMember(d => d.Foo, opt => opt.MapFrom(src => src.Bar.Baz.Foo))
ResolveUsing can be anything
ForMember(d => d.Foo, opt => opt.ResolveUsing(src => HitDatabaseWithStuff(src));
I'd use a Resolver class when the resolution logic needs to be shared amongst more than one member, or if I want to have the resolver instantiated by a service locator. Otherwise, a private method is fine.

Specify a conversion for an interface with automapper

Most of my EF objects have a TenantId Property. The system never have to handle the tenantId it's all taken care for before the SaveChanges(). I want to write an automapper map that will always ignore the TenantId Field.
I've tried
Mapper.CreateMap<IDomainObject, ITenantData>()
.ForMember(m => m.TenantId, a => a.Ignore());
You can handle it using mapping inheritance. For instance:
Mapper.CreateMap<IDomainObject, ITenantData>()
.Include<DomainObject1, TenantData1>()
.ForMember(m => m.TenantId, a => a.Ignore());

Resources