简体   繁体   中英

AutoMapper map long? to string

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 (eg 'Description = description?.Trim();') and validates (eg '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.

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.

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; }
    ...
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM