Automapper does not map from IDataReader to string correctly - automapper

I am using automapper to map from IDataReader. As long as I used it to map from IDataReader to DTOs there was no problem, but after I added a mapping for string, it doesn't work as expected. Take a look at the following sample code I wrote to demonstrate the problem:
static void Main(string[] args)
{
var configuration = new MapperConfiguration(cfg =>
{
//Here Group is one of my DTOs
cfg.CreateMap<IDataReader, Group>()
.ForMember(dest => dest.Id, options => options.MapFrom(src => src["Id"]))
.ForMember(dest => dest.Name, options => options.MapFrom(src => src["Name"]));
//This is the mapping for string type, if I remove it the outcome will not change,
//it seems automapper does not even see this mapping
cfg.CreateMap<IDataReader, string>()
.ConvertUsing(src => src.GetString(0));
});
var mapper = configuration.CreateMapper();
using (SqlConnection connection = new SqlConnection("Data Source=MICHAEL-PC;Initial Catalog=RollingStonesDB;User ID=admin;Password=admin"))
{
connection.Open();
using (SqlCommand command = new SqlCommand("GetSaltByLogin 'jt'", connection))
using (IDataReader reader = command.ExecuteReader())
{
if (reader.Read())
{
//here instead of giving me the expected result it gives me "System.Data.SqlClient.SqlDataReader"
string value = mapper.Map<string>(reader);
}
}
}
}
In this line string value = mapper.Map<string>(reader); I expect to get the actual string value of the first column from the IDataReader, instead I get "System.Data.SqlClient.SqlDataReader". Now the really weird thing is that when I remove this line .ForMember(dest => dest.Name, options => options.MapFrom(src => src["Name"])); from the code above, the mapping will work as expected. For me it seems as if this is a bug. How can elegantly fix this issue so that I will have the mapping for my DTOs and string?

Related

Unit testing that the swagger doc is correct without starting a server

I'd like to test that the swagger document is correct for my application (mainly, because I've added a strategy to generate custom OperationIds and I want to ensure they are correctly unique)
However, the only solutions I found are all using a "real" server (cf https://stackoverflow.com/a/52521454/1545567), which is not an option for me since I do not have the database, message bus, etc... when I launch the unit tests in CI...
At the moment, I have the following but it always generate 0 paths and 0 models ...
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using SampleCheckIn;
using Swashbuckle.AspNetCore.SwaggerGen;
using System;
using System.Linq;
using Xunit;
using SampleCheckIn.Def;
using Service.Utils;
using Swashbuckle.AspNetCore.Swagger;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.FileProviders;
namespace D4Interop.Tests
{
public class TmpTest
{
[Fact]
public void Tmp()
{
var controllers = typeof(Startup).Assembly.GetTypes().Where(x => IsController(x)).ToList();
controllers.Any().Should().BeTrue();
var services = new ServiceCollection();
controllers.ForEach(c => services.AddScoped(c));
services.AddLogging(logging => logging.AddConsole());
services.AddControllers(); //here, I've also tried AddMvcCore and other ASP methods...
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("api", new OpenApiInfo { Title = Constants.SERVICE_NAME, Version = "_", Description = Constants.SERVICE_DESC });
//c.OperationFilter<SwaggerUniqueOperationId>(); //this is my filter that ensures the operationId is unique
c.CustomOperationIds(apiDesc =>
{
return apiDesc.TryGetMethodInfo(out var methodInfo) ? methodInfo.Name : null;
});
});
services.AddSingleton<IWebHostEnvironment>(new FakeWebHostEnvironment());
var serviceProvider = services.BuildServiceProvider();
var swaggerProvider = serviceProvider.GetRequiredService<ISwaggerProvider>();
var swagger = swaggerProvider.GetSwagger("api");
swagger.Should().NotBeNull();
swagger.Paths.Any().Should().BeTrue();
}
private bool IsController(Type x)
{
return typeof(Microsoft.AspNetCore.Mvc.ControllerBase).IsAssignableFrom(x);
}
}
internal class FakeWebHostEnvironment : IWebHostEnvironment
{
public FakeWebHostEnvironment()
{
}
public IFileProvider WebRootFileProvider { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public string WebRootPath { get => "/root"; set => throw new NotImplementedException(); }
public string EnvironmentName { get => "dev"; set => throw new NotImplementedException(); }
public string ApplicationName { get => "app"; set => throw new NotImplementedException(); }
public string ContentRootPath { get => "/"; set => throw new NotImplementedException(); }
public IFileProvider ContentRootFileProvider { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
}
}
Ok, I've finally found that I just need to mix the linked answer with my code :
[Fact]
public async Task TestSwagger()
{
var server = Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(options => { options.UseStartup<Startup>(); })
.Build();
var swagger = server.Services
.GetRequiredService<ISwaggerProvider>()
.GetSwagger("xxx"); //xxx should be the name of your API
swagger.Should().NotBeNull();
swagger.Paths.Any().Should().BeTrue();
swagger.Components.Schemas.Should().NotBeNull();
}

Configuring convention for all members with type in automapper

All my domain models have field public CurrencyId CurrencyId {get; set;}. All my view models have filed public CurrencyVm Currency {get; set;}. Automapper knows how to Map<CurrencyVm>(CurrencyId). How can I setup automatic convention, so I do not have to .ForMember(n => n.Currency, opt => opt.MapFrom(n => n.CurrencyId));?
ForAllMaps is the answer, thanks #LucianBargaoanu. This code kinda works, but hasn't been tested in all cases. Also I do not know how to check if exists mapping between choosen properties.
configuration.ForAllMaps((map, expression) =>
{
if (map.IsValid != null)
{
return; // type is already mapped (or not)
}
const string currencySuffix = "Id";
var currencyPropertyNames = map.SourceType
.GetProperties()
.Where(n => n.PropertyType == typeof(CurrencyId) && n.Name.EndsWith(currencySuffix))
.Select(n => n.Name.Substring(0, n.Name.Length - currencySuffix.Length))
.ToArray();
expression.ForAllOtherMembers(n =>
{
if (currencyPropertyNames.Contains(n.DestinationMember.Name, StringComparer.OrdinalIgnoreCase))
{
n.MapFrom(n.DestinationMember.Name + currencySuffix);
}
});
});

C#: AutoMapper definition is working by Web APi 2 - collection element is not ignored

Im using AutoMapper with EF & Web API 2. Command
tempValue = Mapper.Map<MwbePaymentMethodDtoInOut>(res);
seems to not work. The result object should be object without Payments element , because it's ignored by AutoMapper definition (line: .ForSourceMember(src => src.Payments, opt => opt.Ignore())).
Global.asax
Namespace MobileWallet.Api
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
StartAutomapper();
}
private void StartAutomapper(){
Mapper.Initialize(cfg =>
{
AutoMapperConfiguration.Configure();
});
}
}
}
AutoMapper definition
public class MwbeToDomain : Profile
{
public override string ProfileName
{
get
{
return "MwbeToDomainMapping";
}
}
protected override void Configure()
{
CreateMap<MwbePaymentMethod, MwbePaymentMethodDtoInOut>()
.ForMember(dest => dest.methodType, opt => opt.ResolveUsing<EnumToStringResolver<MwbePaymentMethod.MethodTypeEnum>>().FromMember(source => source.MethodType))
.ForMember(dest => dest.BillingAddress, opt => opt.MapFrom(source => source.BillingAddress))
.ForSourceMember(source => source.UserData, opt => opt.Ignore())
.ForMember(dest => dest.expirationdate, opt => opt.ResolveUsing<DateTimeToString>().FromMember(source => source.ExpirationDate))
.ForSourceMember(src => src.Payments, opt => opt.Ignore())
.ForSourceMember(src => src.Number, opt => opt.Ignore());
}
}
What is wrong with my code?
ADDED:
I added AutoMapper validation right after setting configuration:
private void StartAutomapper(){
//Mapper.AssertConfigurationIsValid();
Mapper.Initialize(cfg =>
{
AutoMapperConfiguration.Configure();
});
Mapper.AssertConfigurationIsValid();
}
Validation code is started by app, but no errors is displayed.
First try adding Mapper.AssertConfigurationIsValid();
AutoMapper: Configuration Validation
I might not be reading this right, but I'm not sure what AutoMapperConfiguration is. Since you are creating a profile, I think you need to tell AutoMapper about it so it knows to use it. Try changing your StartAutomapper function to something like this:
private void StartAutomapper()
{
Mapper.Initialize(cfg =>
{
cfg.AddProfile<MwbeToDomain>();
});
Mapper.AssertConfigurationIsValid();
}
Also, it might help to put a breakpoint in the Profile's Configure method to make sure that it is getting called.
I found solution, my "stupid" fault :).
I had to remove ignored property from destination class. When property was there, it was not ignored by AutoMapper.

Multiple mappings for the same type (different rules)

I am using Automapper to map my domain model (nHibernate) to my view models and back.
Everything is pretty smooth.
Now I need to map an entity (domain entity) to itself cause I want to define some business rules there.
I need to define different mappings for the same type more than once.
I already use profiles for each type of mapping:
public static void Configure()
{
Mapper.Initialize(a =>
{
Mapper.AddProfile(new OrderViewModelMapperProfile());
Mapper.AddProfile(new OrderToOrder1MapperProfile());
Mapper.AddProfile(new OrderToOrder2MapperProfile());
});
}
And this is my profile (simplified):
public class OrderToOrder1MapperProfile : Profile
{
protected override void Configure()
{
this.CreateMap<Domain.Order, Domain.Order>()
.WithProfile("Profile001")
.ForMember(dest => dest.Code, opt => opt.MapFrom(source => System.Guid.Empty))
.ForMember(dest => dest.OrderLines, opt => opt.Ignore())
.AfterMap((source, destination) =>
{
foreach (var line in source.OrderLines)
{
destination.AddOrderLine(Mapper.Map<Domain.OrderLine, Domain.OrderLine>(line));
}
});
this.CreateMap<Domain.OrderLine, Domain.OrderLine>()
.ForMember(dest => dest.Order, opt => opt.Ignore())
.ForMember(dest => dest.Code, opt => opt.MapFrom(source => System.Guid.Empty));
}
}
As you can see I need to do a few things in AfterMap.
My second profile OrderToOrder2MapperProfile looks similar to the one show here so I won't paste the code.
When I map my object:
Domain.Order order = new Domain.Order();
var order2 = Mapper.Map<Domain.Order, Domain.Order>(order);
both profiles are processed.
I decided to follow the path suggested on SO and I've created my "custom" engine:
private IMappingEngine CustomMappingEngine()
{
ConfigurationStore store = new ConfigurationStore(new TypeMapFactory(), MapperRegistry.Mappers);
MappingEngine engine = new MappingEngine(store);
store.AddProfile(new OrderToOrder2MapperProfile());
store.AllowNullDestinationValues = true;
store.AssertConfigurationIsValid();
return (engine);
}
Now everything works fine except for the mapping of the lines; both pipes are processed even if I am using the custom engine to map my object:
Domain.Order order = new Domain.Order();
var newEngine = CustomMappingEngine();
var order2 = newEngine.Map<Domain.Order, Domain.Order>(order);
I guess the problem sits in the fact that I am using the singleton Mapper (engine) here:
destination.AddOrderLine(Mapper.Map<Domain.OrderLine, Domain.OrderLine>(line))
I've tried to find a way to using the current engine inside a profiler but I don't seem to be able to get it in any way.
The only possible solution is to pass the engine in the constructor of my profiler:
public class OrderToOrder2MapperProfile : Profile
{
private readonly IMappingEngine CurrentMappingEngine = null;
public OrderToOrder2MapperProfile(IMappingEngine mappingEngine)
{
this.CurrentMappingEngine = mappingEngine;
}
protected override void Configure()
{
this.CreateMap<Domain.Order, Domain.Order>()
.WithProfile("Profile002")
.ForMember(dest => dest.Code, opt => opt.MapFrom(source => System.Guid.Empty))
.ForMember(dest => dest.OrderLines, opt => opt.Ignore())
.AfterMap((source, destination) =>
{
foreach (var line in source.OrderLines)
{
destination.AddOrderLine(this.CurrentMappingEngine.Map<Domain.OrderLine, Domain.OrderLine>(line));
}
});
this.CreateMap<Domain.OrderLine, Domain.OrderLine>()
.ForMember(dest => dest.Order, opt => opt.Ignore())
.ForMember(dest => dest.Code, opt => opt.MapFrom(source => System.Guid.Empty));
}
}
And use the current engine here:
destination.AddOrderLine(this.CurrentMappingEngine.Map<Domain.OrderLine, Domain.OrderLine>(line));
QUESTION:
Is this the only possible solution or there are better alternatives?
Am I doing things the proper way?
If someone is interested I've created a github repository which uses StructureMap as a test case.

Why use Automappers ValueResolver?

Why do this
Mapper.CreateMap<MyObject, AnotherObject>().
ForMember(x => x.DateAsString, m => m.ResolveUsing<StringToDateTimeFormatter>());
private class StringToDateTimeFormatter : ValueResolver<DateTime, string>
{
protected override string ResolveCore(DateTimesource)
{
return source.ToString("yyyy-MM-dd");
}
}
when you can do this
Mapper.CreateMap<MyObject, AnotherObject>().
ForMember(x => x.DateAsString, m => m.MapFrom(x => x.Date.ToString("yyy-MM-dd")));
???
Update
Here's an example on how to do more complex business logic
Mapper.CreateMap<MyObject, AnotherObject>().
ForMember(x => x.DateAsString, m => m.MapFrom(n => MyMethod(n.DateAsString)));
private object MyMethod(string dateTime)
{
if(!MyDomainObjectIsValid(dateTime))
{
throw new MyValidationException();
}
// do more stuff
}
I still don't see the need for a ValueResolver...
Obviously for your example it is more reasonable to use just MapFrom.
ValueResolvers are needed for more complicated cases. For example when you need to do some validation and throw exception accordingly.
EDIT
ValueResolvers provide access to the destination type and value. Here is small example.
public class FakeResolver : IValueResolver
{
public ResolutionResult Resolve(ResolutionResult source)
{
if (source.Context.DestinationType == typeof(string) && source.Context.DestinationValue == "test")
throw new Exception();
return source;
}
}

Resources