2
votes

I have two classes, a ViewModel and a Dto, that are basically identical except that the Dto has a field 'readonly long? Phone;' while the ViewModel has the a property 'string Phone { get; set; }'.

The only way I've found to get AutoMapper to work is by changing the ViewModel Property to a backing property:

    public long? Phone { get; set; }

    public string PhoneNumberString
    {
        get
        {
            var srv = DependencyResolver.Current.GetService<IPhoneNumberService>();
            return srv.GetFormattedPhoneNumber(Phone);
        }
        set
        {
            var srv = DependencyResolver.Current.GetService<IPhoneNumberService>();
            Phone = srv.GetLongPhoneNumber(value);
        }
    }

And then in AutoMapper, have a gigantic line to call the constructor:

Mapper.Initialize(cfg =>
{
    cfg.CreateMap<MyViewModel, MyDto>()
        .ConstructUsing(src => new MyDto(
            src.Phone
    /* ...Some ~30 other parameters here... */))
        .ReverseMap();
});

...There must be a better way to do this? I've tried these:

.ForSourceMember(x => x.PhoneNumberString, opt => opt.DoNotValidate())

and

.ForMember(x => x.PhoneNumberString, opt => opt.Ignore())

and

.ForMember(viewModel => viewModel.Phone, options => options.MapFrom<PhoneNumberResolver>());//PhoneNumberResolver implements IValueResolver<ProspectMaintenanceViewModel, ProspectMaintenanceDto, long?>

Which all give 'Core.DTO.MyDto needs to have a constructor with 0 args or only optional args.' when trying to map, and:

.ForMember(dest => dest.Phone, opt => opt.MapFrom(src => 5))

Which gives 'System.ArgumentException: 'Expression must be writeable' when trying to configure AutoMapper.

Is there some way I can make AutoMapper understand that it can entirely ignore PhoneNumberString (or, even better, some way by which I can make it map long? to string so I don't need the backing property) without having to use the dto's constructor?

Is there any special reason that requires your DTO to not have a default constructor?

I have all my fields as readonly so that I can include a constructor that modifies (e.g. 'Description = description?.Trim();') and validates (e.g. 'if (Phone.HasValue && Phone.ToString().Length != 10) throw ...') the parameters. This way I can ensure that the Dto, being a value object, is always in a valid state.

2
Is there any special reason that requires your DTO to not have a default constructor? I would just add a default constructor to my data class and trust automapper to fill in all the fields except the ignored ones (the opt.Ignore() solution). - Ishmaeel
Also, you could look into the MapFrom documentation (ResolveUsing in older Automapper versions) to get rid of the PhoneNumberString backing property. This feature allows you to map members of different types (or names) by using external conversion code. - Ishmaeel

2 Answers

2
votes

1) Mapping to readonly field

So you have a Dto class:

public class Dto
{
    public readonly long? PhoneNumber;
}

And then you are trying to force AutoMapper to do this:

var dto = new Dto();
dto.PhoneNumber = 123; // <== ERROR! A readonly field cannot be assigned to.

AutoMapper cannot write to readonly fields or properties. In matter of fact you neither. Either turn your field into a property with protected or private setter:

public class Dto
{
    public long? PhoneNumber { get; private set; }
}

or make it a regular field by removing the readonly keyword:

public class Dto
{
    public long? PhoneNumber;
}

2) Custom value resolving

ASP.NET MVC

Use a ValueResolver:

public class StringPhoneNumberResolver : IValueResolver<Dto, ViewModel, string>
{
    private readonly IPhoneNumberService _phoneNumberService;

    public StringPhoneNumberResolver()
    {
        _phoneNumberService = DependencyResolver.Current.GetService<IPhoneNumberService>();
    }

    public string Resolve(Dto source, ViewModel destination, string destMember, ResolutionContext context)
    {
        return _phoneNumberService.GetFormattedPhoneNumber(source.PhoneNumber);
    }
}

You should know that generally it is an anti-pattern to have service injection in a DTO or IValueResolver. AutoMapper should be dumb and all kind of injections and so on should be handled elsewhere. That being said, here is the AutoMapper configuration:

Mapper.Initialize(cfg =>
{
    cfg.CreateMap<Dto, ViewModel>()
        .ForMember(viewModel => viewModel.PhoneNumber, options =>
            options.MapFrom<StringPhoneNumberResolver>());
});

If you want to reverse the process of long ==> string to string ==> long simply add another value resolver:

public class LongPhoneNumberResolver : IValueResolver<ViewModel, Dto, long?>
{
    private readonly IPhoneNumberService _phoneNumberService;

    public LongPhoneNumberResolver()
    {
        _phoneNumberService = DependencyResolver.Current.GetService<IPhoneNumberService>();
    }

    public long? Resolve(ViewModel source, Dto destination, long? destMember, ResolutionContext context)
    {
        return _phoneNumberService.GetLongPhoneNumber(source.PhoneNumber);
    }
}

.NET Core

If you would operate in .NET Core environment, which fully supports IServiceCollection integration, you should add this AutoMapper configuration:

serviceCollection.AddAutoMapper(config =>
{
    config.CreateMap<Dto, ViewModel>()
        .ForMember(viewModel => viewModel.PhoneNumber, options =>
            options.MapFrom<StringPhoneNumberResolver>());
}, typeof(Startup));

and then have IPhoneNumberServce automagically injected into value resolver:

public StringPhoneNumberResolver(IPhoneNumberService phoneNumberService)
{
    _phoneNumberService = phoneNumberService;
}

For dependency injection I used automapper.extensions.microsoft.dependencyinjection package.

0
votes

Well, I found the problem. It has absolutely nothing to do with what I thought it did. Mapping map long? to string works out of the box.

The problem I had was with an entirely different property.

I had the following structure:

public class MyDto
{
    public readonly AddressDto BillingAddress;
    public readonly AddressDto ShippingAddress;
    public readonly long? Phone;
    ...
}
public class AddressDto
{
    public readonly string Country;
    public readonly string SubnationalEntity;
    ...
}
public class MyViewModel
{
    public string BillingAddressCountry { get; set; }
    public string BillingAddressSubnationalEntity { get; set; }
    public string ShippingAddressCountry { get; set; }
    public string ShippingAddressSubnationalEntity { get; set; }
    public string Phone { get; set; }
    ...
}

It worked once I changed it to the following:

public class MyDto
{
    public readonly AddressDto BillingAddress;
    public readonly AddressDto ShippingAddress;
    public readonly long? Phone;
    ...
}
public class AddressDto
{
    public readonly string Country;
    public readonly string SubnationalEntity;
    ...
}
public class MyViewModel
{
    public string AddressViewModel BillingAddress { get; set; }
    public string AddressViewModel ShippingAddress { get; set; }
    public string Phone { get; set; }
    ...
}
public class AddressViewModel
{
    public string Country { get; set; }
    public string SubnationalEntity { get; set; }
    ...
}