Automapper: Ignoring unmapped members after ConvertUsing

Created on 19 Dec 2016  路  30Comments  路  Source: AutoMapper/AutoMapper

this is my code :
```c#
public class UserProfile:Profile
{
public UserProfile()
{
CreateMap().ConvertUsing();
}

}
public class UserEncryptor : ITypeConverter
{
private readonly IConfigurationRoot _configuration;
public UserEncryptor(IConfigurationRoot configuration)
{
_configuration = configuration;
}

public ApplicationUsers Convert(UserViewModel source, ApplicationUsers destination, ResolutionContext context)
{
    if (context==null||source == null) return null;
    var aes = new Common.EncryptionAes(_configuration[key: "Keys:AesKey"]);
    return new ApplicationUsers
    {
        UserName = aes.EncryptAes(source.Username),
        Email = aes.EncryptAes(source.Email),
        PhoneNumber = aes.EncryptAes(source.MobileNumber),
        User = new User
        {
            FirstName = aes.EncryptAes(source.FirstName),
            LastName = aes.EncryptAes(source.LastName),
            Gender = aes.EncryptAes(source.Gender.ToString()),
            ProfileImage = aes.EncryptAes(source.ProfileImage.FileName)
        }
    };
}

}

**Note that ApplicationUsers is inherited from IdentityUser Class.**
When I tested this mapping,I got this error :

> System.NullReferenceException: Object reference not set to an instance of an object.

I know this error is for that some members are not ignored. Something like this
```c#
CreateMap<UserViewModel ,ApplicationUsers >()
.ConvertUsing(converter=> new ApplicationUsers(){
Email = converter.Email,
....
});

will help me because by default ignore rest of the members but the problem is that if I want to use this kind of code, I cant encrypt my members because I don't access to DI configuration for profile.Because profile is parameter less.

I need something similar to upper code that can implement in ITypeConverter functions.

Anyone has any solution ?

All 30 comments

I'm sorry, what's null exactly?

@jbogard Some IdentityUser peroperties like Claims,Worker object (my custom object), NormalizedEmai, NormalizedUserName, PasswordHash and etc.
When I initialized the rest of the properties with null or something else,I have the same result .

ITypeConverter is more flexible than a Func based converter so you should be able to write the code you need in your type converter. A repro would help. Make a gist that we can execute and see fail.

@lbargaoanu
ApplicationUsers.cs:
```c#
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

namespace PS.ModelBase.Models
{
public class ApplicationUsers : IdentityUser
{
[Column(TypeName = "nvarchar(max)")]
public override string Email
{
get
{
return base.Email;
}

        set
        {
            base.Email = value;
        }
    }
    [Column(TypeName = "nvarchar(max)")]
    public override string UserName
    {
        get
        {
            return base.UserName;
        }

        set
        {
            base.UserName = value;
        }
    }

}

}

**UserViewModel**
```c#
public class UserViewModel
    {
        public string MobileNumber { get; set; }
        public string Email { get; set; }
        public string Username { get; set; }
        public string Password { get; set; }
        public string ReTypePassword { get; set; }
    }

UserProfile.cs

```c#
using System;
using AutoMapper;
using Microsoft.Extensions.Configuration;

namespace PS.ModelBase.MapperProfiles
{
public class UserProfile:Profile
{
public UserProfile()
{
CreateMap().ConvertUsing();
}

}
public class UserConvertor : ITypeConverter<UserViewModel, ApplicationUsers>
{

    public ApplicationUsers Convert(UserViewModel source, ApplicationUsers destination, ResolutionContext context)
    {
        if (context==null||source == null) return null;

        return new ApplicationUsers
        {
            UserName = source.Username,
            Email = source.Email,
            PhoneNumber = source.MobileNumber
        };
    }
}

}

**and this is the testclass :** 
```c#
 using System;
using System.Collections.Generic;
using System.IO;
using AutoMapper;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.AspNetCore.Http;

namespace PS.ModelBaseTest
{   [TestClass]
    public class UserProfileTest
    {
        private readonly IServiceProvider _serviceProvider;
        public UserProfileTest()
        {
            var services = new ServiceCollection();
            services.AddAutoMapper(cfg =>
            {
                cfg.AddProfile<UserProfile>();
            });
            _serviceProvider = services.BuildServiceProvider();
        }

        [TestMethod]
        public void Verify_AutoMapper()
        {
            using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {

                var mapper = serviceScope.ServiceProvider.GetRequiredService<IMapper>();
                var user = new UserViewModel
                {
                    Email = "[email protected]",
                    MobileNumber = "test",
                    Password = "test",
                    ReTypePassword = "test",
                    Username = "test",

                };
               var result= mapper.Map<ApplicationUsers>(user);
                mapper.ConfigurationProvider.AssertConfigurationIsValid();
            }

        }
    }
}

I suppose what you mean is that DI is not working for you. See https://github.com/AutoMapper/AutoMapper.Extensions.Microsoft.DependencyInjection. UserEncryptor is not registered with your container.

I guess I was wrong, the package should do that for you. So I'm missing your point. That's way too much code. Isolate things to a gist that depends only on AM.

@lbargaoanu I registered DI in my test constructor.
OK I will ...
Please wait.

@lbargaoanu I edited the previous codes and made a gist ...

I was able to run smth close to your code. What I see is that the members you set have the right values and the rest the defaults. So what's wrong? :) Modify my gist and add asserts to highlight the problem.

@lbargaoanu Ok thank you for your code . I cant work perfectly with Github and I dont know how can I add assert to highlight your code . but the point of your code was this :

var mapper = config.CreateMapper();

When I modify my code and doing something like you do ,in my test I have one change:

var mapper = serviceScope.ServiceProvider.GetRequiredService<IMapper>().ConfigurationProvider.CreateMapper();

Every thing is going fine and you are right, But when I create a constructor for UserConvertor for adding configuration DI, I got another error :

Result StackTrace:
at lambda_method(Closure )
at AutoMapper.Mappers.ObjectCreator.CreateObject(Type type)
at AutoMapper.MappingOperationOptions`2.CreateInstanceT
at lambda_method(Closure , Object , Object , ResolutionContext )
at AutoMapper.Mapper.AutoMapper.IMapper.MapTDestination
at PS.ModelBaseTest.UserProfileTest.Verify_AutoMapper_Encryption()

Result Message:
Test method PS.ModelBaseTest.UserProfileTest.Verify_AutoMapper threw exception:
System.ArgumentException: PS.ModelBase.MapperProfiles.UserConvertor needs to have a constructor with 0 args or only optional args
Parameter name: type

```c#
public class UserConvertor : ITypeConverter
{
private readonly IConfigurationRoot _configuration;
public UserConvertor(IConfigurationRoot configuration)
{
_configuration = configuration;
}

    public ApplicationUsers Convert(UserViewModel source, ApplicationUsers destination, ResolutionContext context)
    {
        if (context==null||source == null) return null;

        var aes = new Common.EncryptionAes(_configuration[key: "Keys:AesKey"]);
        return new ApplicationUsers
        {
            UserName = aes.EncryptAes(source.Username),
            Email = aes.EncryptAes(source.Email),
            PhoneNumber = aes.EncryptAes(source.MobileNumber)
        };
    }

```

Is there any way to using DI in that method ?

So DI is the problem after all. The code you had initially with CreateScope looked reasonable. Doesn't that work?

@lbargaoanu for another scenarios, DI worked without any problem, but when the destination class is inherited from IdentityUser, it does't work ....

It's still not clear what the actual error is.

@lbargaoanu
the last error was this :

System.ArgumentException: PS.ModelBase.MapperProfiles.UserConvertor needs to have a constructor with 0 args or only optional args
Parameter name: type

the first error was this :

System.NullReferenceException: Object reference not set to an instance of an object.

I'm getting mad of this :(

@lbargaoanu either I cant use DI in Profile constructor ....

That doesn't help. The first error is because that last code you posted is broken, it forces AM to create the resolver and that fails. The initial error is too generic. It's up to you to debug your app and isolate the issue. Anyway, at this point it's pretty clear the problem is not with AM. It's your code or the DI AM package. But again, it's hard to tell.

Wait, you have this?

var mapper = serviceScope.ServiceProvider.GetRequiredService<IMapper>().ConfigurationProvider.CreateMapper()

?

@jbogard Yes, after the guidance of @lbargaoanu,I added this to my code and I got another error :

System.ArgumentException: PS.ModelBase.MapperProfiles.UserConvertor needs to have a constructor with 0 args or only optional args
Parameter name: type

You should only go from GetRequiredService, not IMapper.ConfigurationProvider.CreateMapper. Have you looked at the example in the AutoMapper DI repo? It shows this exact scenario.

@jbogard Yes I read that.In the first code,It didn't have the 'IMapper.ConfigurationProvider.CreateMapper', this worked for another scenarios but when the destination class is inherited from IdentetyUser, it doesn't work and this error shows :

System.NullReferenceException: Object reference not set to an instance of an object.

OK I'm getting totally confused about your isolated issue. Do you have a link to a VS solution with your original problem?

@jbogard
https://drive.google.com/open?id=0B6ty2MRAzdqkVFdrd3VXZ09LaGs

It's worked:
PS.Common->MappingProfiles->LogProfile.cs
PS.CommonTest->LogProfileTest.cs

It's not worked :
PS.ModelBase->MappingProfiles->UserProfile.cs
PS.ModelBaseTest->UserProfileTest.cs

Yep, definitely not working for me locally. Just to eliminate some variables, I copied the registration code straight into the test so that I can debug locally.

OK I found the issue - when you don't pass in the assemblies you want to scan for registering type converters, value resolvers etc. to AddAutoMapper, it will look in a special place to determine what assemblies to look for AutoMapper classes, a "DependencyContext" (by default, DependencyContext.Default). This replaces the AppDomain way of probing the current running context for "what assemblies are loaded".

I go through these assemblies and figure out which ones to search the types for AutoMapper types. Searching through EVERY type would be quite expensive, and in fact, in your app there are > 200 assemblies in the dependency context!

Instead, I try to be smart and look at the assembly's references to see if it references AutoMapper.

The problem is I only search one level deep, and your test project does not directly reference AutoMapper. Instead, it's a dependency of a dependency. And unfortunately, it would be pretty hard to trawl through that tree to figure out what references what and who actually uses AutoMapper.

There are two easy ways to fix this:

  • Reference AutoMapper in your test assembly
  • Explicitly pass in the array of assemblies to probe for AutoMapper config <- THIS IS THE MOST EXPLICIT AND OBVIOUS

One last thing - I probe and find Profiles automatically, so your "AddProfile" is redundant. I made the test pass by changing your config to:

services.AddAutoMapper(typeof(UserProfile));

@jbogard Thank you for taking your time and your perfect explanations.
Finally the problem was solved :))

@jbogard I've added a null check to CreateInstance for this exact scenario. Did I miss smth or the AM version is before that fix? 5.2.0 should have it I believe.

It's 5.0.2 as that's what the DI package references. This is a bug in the DI extension library, so I can push out an update that uses the latest 5.2 version.

This can be closed.

This issue was moved to AutoMapper/AutoMapper.Extensions.Microsoft.DependencyInjection#11

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings