After previous question I have a simple implementation of IValueResolver
public class FileLinkResolver : IValueResolver<Configuration, ConfigurationDto, string>
{
private readonly IFileStorage _fileStorage;
public FileLinkResolver(IFileStorage fileStorage)
{
_fileStorage = fileStorage;
}
public string Resolve(Configuration source, ConfigurationDto destination, string destMember, ResolutionContext context)
{
return _fileStorage.GetShortTemporaryLink(source.Path);
}
}
and simple mapping profile
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<Configuration, ConfigurationDto>()
.ForMember(dest => dest.FilePath, opt => opt.MapFrom<FileLinkResolver>());
}
}
For production it works as expected when the following setup
services.AddTransient<IFileStorage>(...);
services.AddAutoMapper();
is used and then in controller IMapper is injected.
In unit test I try to stub the mapper
var mapperStub = new Mapper(new MapperConfiguration(map => map.AddProfile(new MappingProfile())));
and when I run tests for method witch should return mapped dto, I got
AutoMapper.AutoMapperMappingException : Error mapping types.
Mapping types:
Configuration -> ConfigurationDto
DataAccess.Models.Configuration -> Dto.ConfigurationDto
Type Map configuration:
Configuration -> ConfigurationDto
DataAccess.Models.Configuration -> Dto.ConfigurationDto
Destination Member:
FilePath
---- System.MissingMethodException : No parameterless constructor defined for this object.
I've tried to add parameterless constructor to FileLinkResolver but then, NullReferenceException
This is the question: how to resolve dependencies for ValueResolver
In the current example, the mapper is unable to resolve IFileStorage dependency when testing.
Change the way the mapper is created to more closely match how it is done at run-time.
IServiceCollection services = new ServiceCollection();
//mocking service using MOQ
var mock = Mock.Of<IFileStorage>(_ =>
_.GetShortTemporaryLink(It.IsAny<string>()) == "fake link"
);
//adding mock to service collection.
services.AddTransient<IFileStorage>(sp => mock);
//adding auto mapper with desired profile;
services.AddAutoMapper(typeof(MappingProfile));
//...add other dependencies as needed to service collection.
//...
//build provider
IServiceProvider serviceProvider = service.BuilderServiceProvider();
//resolve mapper
IMapper mapperStub = serviceProvider.GetService<IMapper>();
//Or resolve subject under test where mapper is to be injected
SubjectClass subject = serviceProvider.GetService<SubjectClass>();
//assuming ctr: public SubjectClass(IMapper mapper, .....) { ... }
Now technically this is not mocking the value resolver. It mocks the dependencies of the resolver, and uses an actual resolver from the profile. But this should provide the desired behavior when testing the target.
Related
I am trying to map from a db-model to a view model. For one property I need a custom value resolver.
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Model.Db.Kontoauszug, KontoauszugDetailViewModel>()
.ForMember(dest => dest.IsTeamleiter, opt => opt.MapFrom<KontoauszugIsTeamleiterResolver>());
});
var mapper = new Mapper(config);
return mapper.Map<KontoauszugDetailViewModel>(kontoauszug);
The custom value resolver is dependent on a service as you can see here:
public class KontoauszugIsTeamleiterResolver : IValueResolver<Model.Db.Kontoauszug, KontoauszugDetailViewModel, bool>
{
private readonly ISysParamService sysParamService;
public KontoauszugIsTeamleiterResolver(ISysParamService sysParamService)
{
this.sysParamService = sysParamService;
}
public bool Resolve(Model.Db.Kontoauszug source, KontoauszugDetailViewModel destination, bool destMember, ResolutionContext context)
{
var teamleiter = this.sysParamService.GetParamValueAs<string>(KontoauszugSysParamConst.KONTOAUSZUG_TEAMLEITER_MANUMMERN).Split(";").ToList();
return teamleiter.Contains(source.MitarbeiterNr);
}
}
Unfortuantely, when running this code throws a exception that the valueresolver does not have a parameterless constructor.
I'm using the standard .net core dependency injection and in my Startup.cs I'm registering the automappers via
services.AddAutoMapper(typeof(Startup));
I've also tried to explicitly register the value resolver:
services.AddScoped<IValueResolver<Model.Db.Kontoauszug, KontoauszugDetailViewModel, bool>, KontoauszugIsTeamleiterResolver>();
But it does not work.
What am I doing wrong that this exception is thrown?
Thank in advance
I have a .NET Core 3.1 API with the following Nuget packages:
AutoMapper (10.1.1)
AutoMapper.Extensions.Microsoft.DependencyInjection (8.1.1)
I'm trying to map a value from an entity to a dto using a ValueResolver and I'm having an exception:
AutoMapperMappingException: Cannot create an instance of type TestAutomapperResolver.Mapping.CustomResolver
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAutoMapper(cfg => cfg.AddMaps(typeof(TestProfile).Assembly));
}
TestProfile.cs
public class TestProfile : Profile
{
public TestProfile()
{
CreateMap<TestEntity, TestDto>()
.ForMember(src => src.PropertyToBeMappedByResolver, opts => opts.MapFrom<CustomResolver>());
}
}
public class CustomResolver : IValueResolver<TestEntity, TestDto, string>
{
public string Resolve(TestEntity source, TestDto destination, string destMember, ResolutionContext context)
{
return "String generated with resolver";
}
}
When doing mapper.CreateMap<TestDto>(entity); I'm receiving that exception.
By the way, using this resolver as opts => opts.MapFrom(CustomResolver()) is not an option because I want to inject some service into that resolver.
Any idea?
You're using AddMaps when you shouldn't be. AddMaps just adds profiles and mappings, but doesn't add all the extra services that the DI package does.
This will do it properly:
services.AddAutoMapper(typeof(TestProfile).Assembly);
Now, AutoMapper gives you a very unhelpful error here, but the issue goes back to Microsoft Dependency injection. The DI doesn't know about your custom resolver type, so it doesn't even try.
Since you didn't use the DI package's extension method, the resolver doesn't get added to the service collection. You can manually add these services if needed:
services.AddAutoMapper(cfg => cfg.AddMaps(typeof(TestProfile).Assembly));
services.AddTransient<CustomResolver>();
I'm developing a REST api. I implement automapper packets to compare the post model object with a correct user model object.
The problem is that I configured and use like service in the dependency injection, but ignore the defined rules.
IMapper mapper = CreateMapperStartUp(services);
Here I created the IMapper object:
private IMapper CreateMapperStartUp(IServiceCollection services)
{
var mapperConfiguration = new MapperConfiguration(conf =>
{
conf.AddProfile<MappingKey>();
});
IMapper mapper = mapperConfiguration.CreateMapper();
services.AddAutoMapper(typeof(Startup));
return mapper;
}
I'm using a profile "MappingKey".
public class MappingKey : Profile
{
public MappingKey()
{
CreateMap<CreateKeyViewModel, Key>().ReverseMap();
CreateMap<UpdateKeyViewModel, Key>().ReverseMap();
}
}
There I created the map method with post object models and key user objects. And finally my validator class with mapping rules.
public class CreateKeyValidator : AbstractValidator<CreateKeyViewModel>
{
private readonly MysqlDAO _mysqlDAO;
public CreateKeyValidator(MysqlDAO mysqlDAO)
{
_mysqlDAO = mysqlDAO;
RuleFor(x => x.Id).NotNull().Must(x => !_mysqlDAO.ExistId(x));
RuleFor(x => x.Colour).NotNull().Length(2, 3);
RuleFor(x => x.Date).NotNull().Length(2, 50);
}
}
What is the problem and why the mapper pass object with null parameters?
If I correctly understand. You create mapper. And do not use it anywhere (unfortunately ther is no using in code that you provide).
You call services.AddAutoMapper(typeof(Startup));
But mapper that you created IMapper mapper and init it's profile, not used here. So, how your application will knows about mapper instance.
Is I correct? If not please provide more context.
I suppose you should write something like this:
IMapper mapper = mappingConfig.CreateMapper();
services.AddSingleton(mapper);
Ok, finally found the mistake. In this case the validation class wasn't defined by service on the StartUp. So using the fluentValidation method to create his own instance:
services.AddMvc().AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Startup>());
And that's it. It works fine all the rules.
Yes, in the post I don't wrote the AddSingleton, but in the real pipeline it's like that.
private IMapper CreateMapperStartUp(IServiceCollection services) {
var mapperConfiguration = new MapperConfiguration(conf =>
{
// conf.CreateMap<Key, CreateKeyViewModel>();
conf.AddProfile<MappingKey>();
});
IMapper mapper = mapperConfiguration.CreateMapper();
services.AddAutoMapper(typeof(Startup));
// services.AddSingleton(mapper);
return mapper;
}
And when I'm adding the instance to the services, I need to access the registered instance of IMapper.
private void AddMysqlService(IServiceCollection services)
{
ILoggerFactory loggerFactory = CreateLoggerFactory(services.BuildServiceProvider());
IMapper mapper = CreateMapperStartUp(services);
services.AddSingleton<IConnection<Key>>(new ConnectionMannager(Configuration.GetSection("mysqlDb"), loggerFactory));
var connectionMnamagerInstance = services.BuildServiceProvider().GetService<IConnection<Key>>();
services.AddSingleton<IService<Key>>(new MysqlService(mapper, connectionMnamagerInstance));
}
In some cases one of my application services has to generate DTOs with anonymized data for the frontend. The idea was to use different AutoMapper profiles to either map the domain object to the DTO with all properties mapped or the anonymized DTO.
I generated these two profiles and injected them into the service. The AutoMapper is also injected into the service as IMapper and contains all mapping profiles of the application.
What I need now is to tell the mapper to use one specific profile in a call of the Map-function.
Something like this:
var anonymizedDto = _autoMapper.Map<SourceType, DestinationType>
(sourceObject, ops => ops.UseMappingProfile(_anonymizedMapingProfile));
var normalDto = _autoMapper.Map<SourceType, DestinationType>
(sourceObject, ops => ops.UseMappingProfile(_normalMappingProfile));
Is this possible and if yes: how?
As far as i know, you can not change the profile when you call Map.
What you can do is inject two mappers that have been configured with different profiles.
public class MyService : IService {
private readonly IMappingEngine _defaultMapper;
private readonly IMappingEngine _anonymousMapper;
public MyService(IMappingEngine defaultMapper, IMappingEngine anonymousMapper) {
_defaultMapper = defaultMapper;
_anonymousMapper = anonymousMapper;
}
public MyDto GetDefault() {
return _defaultMapper.Map<MyDto>(sourceObject);
}
public MyDto GetAnonymous() {
return _anonymousMapper.Map<MyDto>(sourceObject);
}
}
In your dependency container, set up the constructor injection to respect the name of the ctor parameter. For example with StructureMap:
public void ConfigureAutoMappers(ConfigurationExpression x) {
// register default mapper (static mapping configuration)
Mapper.Configuration.ConstructServicesUsing(t => container.GetInstance(t));
Mapper.Configuration.AddProfile<DefaultProfile>();
var defaultAutomapper = Mapper.Engine
x.For<IMappingEngine>().Use(() => defaultAutoMapper).Named("DefaultAutoMapper");
// register anonymous mapper
var anonConfig = new AnonConfigurationStore( // class derived from ConfigurationStore
new TypeMapFactory(),
AutoMapper.Mappers.MapperRegistry.AllMappers()
);
anonConfig.ConstructServicesUsing(container.GetInstance);
var anonAutoMapper = new MappingEngine(anonConfig);
x.For<IMappingEngine>().Add(anonAutoMapper).Named("AnonAutoMapper");
// Inject the two different mappers into our service
x.For<IService>().Use<MyService>()
.Ctor<IMappingEngine>("defaultMapper").Named("DefaultAutoMapper")
.Ctor<IMappingEngine>("anonymousMapper").Named("AnonAutoMapper");
}
Whats the best way to setup a mock expection for the Map function in AutoMapper.
I extract the IMapper interface so I can setup expects for that interface. My mapper has dependencies, so I have to pass those in to the mapper.
What happens when I create 2 instances of my mapper class, with 2 different dependency implementations? I asume that both mappers will use the same dependency instance, since the AutoMapper map is static. Or AutoMapper might even throw an exception because I try to setup 2 different maps with the same objects.?
Whats the best way to solve this?
public interface IMapper {
TTarget Map<TSource, TTarget>(TSource source);
void ValidateMappingConfiguration();
}
public class MyMapper : IMapper {
private readonly IMyService service;
public MyMapper(IMyService service) {
this.service = service
Mapper.CreateMap<MyModelClass, MyDTO>()
.ForMember(d => d.RelatedData, o => o.MapFrom(s =>
service.getData(s.id).RelatedData))
}
public void ValidateMappingConfiguration() {
Mapper.AssertConfigurationIsValid();
}
public TTarget Map<TSource, TTarget>(TSource source) {
return Mapper.Map<TSource, TTarget>(source);
}
}
You don't need to mock AutoMapper, you can just inject the real one as explained here:
var myProfile = new MyProfile();
var configuration = new MapperConfiguration(cfg => cfg.AddProfile(myProfile));
IMapper mapper = new Mapper(configuration);
You can inject this mapper in your unit tests. The whole point of using tools like AutoMapper is for you not having to write a lot of mapping code. If you mock AutoMapper you'll end up having to do that.
Whats the best way to setup a mock expection for the Map function in AutoMapper[?]
Here's one way:
var mapperMock = new Mock<IMapper>();
mapperMock.Setup(m => m.Map<Foo, Bar>(It.IsAny<Foo>())).Returns(new Bar());
http://richarddingwall.name/2009/05/07/mocking-out-automapper-with-dependency-injection/
Points out another way of handling dependencies to AutoMapper, which basically will replace my attempt to extract my own IMapper interface. Instead I will attempt to use the existing IMappingEngine as dependency for my classes, to see if that works.
What you need to do is setup AutoMapper like this (StructureMap is IoC). Then you can make your services dependent on IMappingEngine. From there mocking it should be very easy.
public class AutoMapperConfigurationRegistry : Registry
{
public AutoMapperConfigurationRegistry()
{
ForRequestedType<Configuration>()
.CacheBy(InstanceScope.Singleton)
.TheDefault.Is.OfConcreteType<Configuration>()
.CtorDependency<IEnumerable<IObjectMapper>>().Is(expr => expr.ConstructedBy(MapperRegistry.AllMappers));
ForRequestedType<ITypeMapFactory>().TheDefaultIsConcreteType<TypeMapFactory>();
ForRequestedType<IConfigurationProvider>()
.TheDefault.Is.ConstructedBy(ctx => ctx.GetInstance<Configuration>());
ForRequestedType<IConfiguration>()
.TheDefault.Is.ConstructedBy(ctx => ctx.GetInstance<Configuration>());
}
}
The reason you have to invoke automapper config is because, the UNIT Test cases instance runs outside of main application start up files/configs. Hence the auto mapper configuration has to be called and setup before any unit tests start to run. Ideally you place it in TestInitialize methods.
Here is an example of how I provide an instance of AutoMapper as default Mock for IMapper, using AutoFixture and Moq:
Thanks Lucian Bargaoanu for this hint: Actually you can use cfg.AddMaps(params Assembly[]) and Automapper will search for profiles
Create an ICustomization
public class MapperCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Register<IMapper>(() =>
{
var configuration = new MapperConfiguration(cfg =>
{
cfg.AddMaps(
Assembly.Load("BookSharing.Application"),
Assembly.Load("BookSharing.Infrastructure"));
});
return new Mapper(configuration);
});
}
}
Register the customization
fixture.Customize(new MapperCustomization());
following #Dorin Baba answer, I created a useful generic class that can be used to inject any custom mapper in the unit tests
public class MapperCustomization<T> : ICustomization
where T : class
{
public void Customize(IFixture fixture)
{
fixture.Register<IMapper>(() =>
{
var config = new MapperConfiguration(cfg =>
{
cfg.AddMaps(
typeof(T).Assembly);
});
return new Mapper(config);
});
}
}