Search code examples
c#asp.net-coredomain-driven-designautomapperabp-framework

Missing type map configuration or unsupported mapping when mapping from a JSON string to IEnumerable of strings


I have a domain model representing a user and roles assigned to the user:

public class UserRoles : AggregateRoot<Guid>
{
    public string Email { get; private set; }
    public IEnumerable<string> Roles { get; private set; }

    public UserRoles(Guid id, string email, IEnumerable<string> roles) : base(id)
    {
        Email = email;
        Roles = roles;
    }
}

And I'm storing the roles assigned to the user as a JSON string instead, in a PostgreSQL as jsonb column type. Here is the database entity:

public class UserRolesEntity
{
    public Guid Id { get; set; }
    public string Email { get; set; } = null!;
    public string Roles { get; set; } = null!;
}

The Roles string is stored as something like the following in the database:

["administrator", "engineer", "manager"]

The database layer is using EntityFrameworkCore, with PostgreSql package.

Assuming I have all necessary things setup correctly: DbContext, tables, columns, etc. Here is the repository to get data from the database and convert it back to domain models:

public class UserRolesRepository : IUserRolesRepository
{
    private readonly IamDbContext _iamDbContext;
    private readonly IObjectMapper _objectMapper;

    public UserRolesRepository(...)
    {...}

    public async Task<UserRoles?> GetByEmailAsync(string email, CancellationToken...)
    {
        var dbEntity = await _iamDbContext.UserRolesList
            .SingleOrDefaultAsync(x => x.Email == email, cancellationToken);

        if (dbEntity == null)
        {
            return null;
        }

        return _objectMapper.Map<UserRolesEntity, UserRoles>(dbEntity);
    }
}

IObjectMapper is the mapper interface from ABP. And I am using AutoMapper as its implementation. Here is the mapping profile:

public class DbEntityToDomainModelProfile : Profile
{
    public DbEntityToDomainModelProfile()
    {
        CreateMap<UserRolesEntity, UserRoles>()
            .ForMember(dest => dest.Roles, opt => opt.MapFrom<JsonStringToUserRolesResolver>())
            .Ignore(dest => dest.ConcurrencyStamp)
            .Ignore(dest => dest.ExtraProperties)
            .DisableCtorValidation();
    }
}

public class JsonStringToUserRolesResolver : IValueResolver<UserRolesEntity, UserRoles, IEnumerable<string>>
{
    public IEnumerable<string> Resolve(UserRolesEntity source, UserRoles dest, 
        IEnumerable<string> destMember, ResolutionContext context)
    {
        // System.Text.Json
        var userRoles = JsonSerializer.Deserialize<IEnumerable<string>>(source.Roles);
        if (userRoles == null)
        {
            return Enumerable.Empty<string>();
        }

        return userRoles;
    }
}

I have this value resolver in place to deserialize the JSON string I store in the database, into an IEnumerable of strings, but when I run it, I am getting:

AutoMapper.AutoMapperMappingException: Missing type map configuration or unsupported mapping.

Mapping types:
String -> IEnumerable`1
System.String -> System.Collections.Generic.IEnumerable`1

This is a simplified version of the setup I have: https://dotnetfiddle.net/75ZnMc.

With the value resolver, I was expecting the JSON string would be successfully deserialized into an IEnumerable of strings.


Work-around #1

https://dotnetfiddle.net/eO6jvj

If I rename the roles parameter in the UserRoles constructor to something else, and add a parameter-less constructor there, it will work. But I can't explain why!

public class UserRoles : AggregateRoot<Guid>
{
    ...

    private UserRoles() { }

    public UserRoles(Guid id, string email, IEnumerable<string> roleList) : base(id)
    {
        Email = email;
        Roles = roleList;
    }
}

Solution

  • The reason of that exception is that the AutoMapper can't find constructor parameter with matching configured mapping.

    The AutoMapper just doesn't support IValueResolver for constructor parameters. https://github.com/AutoMapper/AutoMapper/issues/2549

    You have two options to achieve what you want:

    • Use .ForCtorParam("roles", opt => opt.MapFrom(src => JsonSerializer.Deserialize<IEnumerable<string>>(src.Roles, new JsonSerializerOptions()))); instead of .ForMember
    • Add default constructor to destination type to make possible to create empty object and fill it's values

    P. S. I've created AutoMapper extensions which simplify the code. You may write CreateMap<T1, T2>().From(...).To(...) instead of ForMember({long expression configuration here}). Here is the link https://www.nuget.org/packages/PetrolMuffin.AutoMapper.Extensions