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.
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;
}
}
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:
.ForCtorParam("roles", opt => opt.MapFrom(src => JsonSerializer.Deserialize<IEnumerable<string>>(src.Roles, new JsonSerializerOptions())));
instead of .ForMember
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