Переписывает ли AutoMapper конфигурацию?

Я создаю трехслойное приложение на основе Entity Framework и AutoMapper. У меня есть слой DAL, BLL и Presentation. Вот мои объекты для уровня доступа к данным:

[Table("Person")]
public class Person
{
    [Key]
    public virtual long Id { get; set; }
    [Column("Pib")]
    public virtual string Pib { get; set; }
    [Column("Bd")]
    public virtual Nullable<DateTime> BirthdaySingle { get; set; }
    [Column("Bp")]
    public virtual string BirthPlace { get; set; }
    [Column("Other")]
    public virtual string Other { get; set; }
    public virtual ICollection<Photo> Photos { get; set; }
}

[Table("Photo")]
public class Photo
{
    [Key]
    public virtual long Id { get; set; }
    [Column("Ph")]
    public virtual byte[] RealPhoto { get; set; }
    public virtual Nullable<long> PersonId { get; set; }
    [ForeignKey("PersonId")]
    public virtual Person Person { get; set; }
}

Для уровня бизнес-уровня:

public class PersonDTO
{
    public virtual long Id { get; set; }
    public virtual string Pib { get; set; }
    public virtual Nullable<DateTime> BirthdaySingle { get; set; }
    public virtual string BirthPlace { get; set; }
    public virtual string Other { get; set; }
    public virtual ICollection<PhotoDTO> Photos { get; set; }
}

public class PhotoDTO
{
    public virtual long Id { get; set; }
    public virtual byte[] RealPhoto { get; set; }
    public virtual Nullable<long> PersonId { get; set; }
    public virtual PersonDTO PersonDTO { get; set; }
}

И для уровня представления:

// class for showing details
public class PersonViewModel
{
    public virtual long Id { get; set; }
    public virtual string Pib { get; set; }
    public virtual Nullable<DateTime> BirthdaySingle { get; set; }
    public virtual string BirthPlace { get; set; }
    public virtual string Other { get; set; }
    public virtual ICollection<PhotoViewModel> Photos { get; set; }

    public override string ToString()
    {
        string result = string.Empty;

        if (!string.IsNullOrWhiteSpace(Pib))
            result = string.Format("\r\n{0}", Pib);
        if (BirthdaySingle.HasValue)
            result += string.Format("\r\n{0}", BirthdaySingle.Value.ToShortDateString());
        if (!string.IsNullOrWhiteSpace(BirthPlace))
            result += string.Format("\r\n{0}", BirthPlace);
        if (!string.IsNullOrWhiteSpace(Other))
            result += string.Format("\r\n{0}", Other);

        return result;
    }
}

// class for showing list of objects
public class PersonListViewModel
{
    public class PersonShortViewModel
    {
        [DisplayName("#")]
        public virtual long Id { get; set; }
        [DisplayName("Full Name")]
        public virtual string Pib { get; set; }
        [DisplayName("Birth Date")]
        [DisplayFormat(DataFormatString = "{0:dd.MM.yyyy}")]
        public virtual Nullable<DateTime> BirthdaySingle { get; set; }
    }

    public IPagedList<PersonShortViewModel> Persons { get; set; } 
}

public class PhotoViewModel
{
    public virtual long Id { get; set; }
    public virtual byte[] RealPhoto { get; set; }
    public virtual Nullable<long> PersonId { get; set; }
}

Итак, у меня есть класс DataService в BLL:

public class DataService : IDataService
{
    IUnitOfWork Database { get; set; }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="T:System.Object"/>.
    /// </summary>
    public DataService(IUnitOfWork database)
    {
        //AutoMapperBLLConfiguration.Configure();
        Database = database;
    }

    public bool IsConnected()
    {
        return Database.IsConnected();
    }

    public IQueryable<PersonDTO> GetPersons()
    {
        Mapper.CreateMap<Person, PersonDTO>().ForMember(ph => ph.Photos, opt => opt.Ignore());
        return Database.Persons.GetAll().ProjectTo<PersonDTO>();
    }

    public PersonDTO GetPerson(long id)
    {
        var person = Database.Persons.GetById(id);
        if (person == null)
        {
            throw new ValidationException("Об'єкт не знайдено.", "");
        }

        Mapper.CreateMap<Photo, PhotoDTO>();
        Mapper.CreateMap<Person, PersonDTO>().ForMember(pe => pe.Photos, opt => opt.MapFrom(p => p.Photos));
        return Mapper.Map<PersonDTO>(person);
    }

    public IEnumerable<PersonDTO> GetPersonsBy(Expression<Func<PersonDTO, bool>> predicate)
    {
        if (predicate == null)
        {
            throw new ValidationException("Відсутня умова пошуку.", "");
        }

        Mapper.CreateMap<PersonDTO, Person>().ForMember(person => person.CbdId, opt => opt.Ignore());
        return
            Mapper.Map<IEnumerable<PersonDTO>>(
                Database.Persons.GetByCondition(predicate.RemapForType<PersonDTO, Person, bool>()));
    }

    public PhotoDTO GetPhoto(long id)
    {
        var photo = Database.Photos.GetById(id);
        if (photo == null)
        {
            throw new ValidationException("Зображення не знайдено.", "");
        }

        return Mapper.Map<PhotoDTO>(photo);
    }

    public IEnumerable<PhotoDTO> GetPhotosBy(Expression<Func<PhotoDTO, bool>> predicate)
    {
        if (predicate == null)
        {
            throw new ValidationException("Відсутня умова пошуку.", "");
        }
        Expression<Func<Photo, bool>> mappedSelector = Mapper.Map<Expression<Func<Photo, bool>>>(predicate);
        return Mapper.Map<IEnumerable<PhotoDTO>>(Database.Photos.GetByCondition(mappedSelector));
    }

    public void Dispose()
    {
        Database.Dispose();
    }
}

Я решил создать отдельную конфигурацию для AutoMapper, потому что я думаю, что уровень представления не должен знать о BLL...

public static class AutoMapperBLLConfiguration
{
    public static void Configure()
    {
        Mapper.Initialize(configuration =>

               /*configuration.AddProfile(new PhotoIgnoreProfile());
               configuration.AddProfile(new PhotoIncludeProfile());*/
               /*configuration.AddProfile(new PhotoProfile());
               configuration.AddProfile(new PersonProfile());*/
               GetConfiguration(Mapper.Configuration)
            );
        Mapper.AssertConfigurationIsValid();
    }

    private static void GetConfiguration(IConfiguration configuration)
    {
        var profiles =
            typeof(PhotoProfile).Assembly.GetTypes().Where(type => typeof(Profile).IsAssignableFrom(type));
        foreach (Type profile in profiles)
        {
            configuration.AddProfile(Activator.CreateInstance(profile) as Profile);
        }
    }
}

public class PersonProfile : Profile
{
    protected override void Configure()
    {
        Mapper.CreateMap<Person, PersonDTO>();
        Mapper.CreateMap<PersonDTO, Person>().ForMember(person => person.CbdId, opt => opt.Ignore());
    }
}

/*public class PhotoIgnoreProfile : Profile
{
    protected override void Configure()
    {
        Mapper.CreateMap<Person, PersonDTO>().ForMember(ph => ph.Photos, opt => opt.Ignore());
    }
}

public class PhotoIncludeProfile : Profile
{
    protected override void Configure()
    {
        Mapper.CreateMap<Person, PersonDTO>().ForMember(pe => pe.Photos, opt => opt.MapFrom(p => p.Photos));
    }
}*/

public class PhotoProfile : Profile
{
    protected override void Configure()
    {
        Mapper.CreateMap<Photo, PhotoDTO>().ForMember(dto => dto.PersonDTO, opt => opt.MapFrom(photo => photo.Person));
        Mapper.CreateMap<PhotoDTO, Photo>().ForMember(photo => photo.Person, opt => opt.MapFrom(dto => dto.PersonDTO));
    }
}

И для слоя представления:

public static class AutoMapperPLConfiguration
{
    public static void Configure()
    {
        Mapper.Initialize(configuration =>

                //configuration.AddProfile(new PersonViewProfile());
                //configuration.AddProfile(new PhotoViewProfile());
                GetConfiguration(Mapper.Configuration)
            );
        Mapper.AssertConfigurationIsValid();
    }

    private static void GetConfiguration(IConfiguration configuration)
    {
        // we use order by because we need photo mapping to be the first
        var profiles =
            typeof(PhotoViewProfile).Assembly.GetTypes().Where(type => typeof(Profile).IsAssignableFrom(type)).OrderByDescending(type => type.Name);
        foreach (Type profile in profiles)
        {
            configuration.AddProfile(Activator.CreateInstance(profile) as Profile);
        }
    }
}

public class PersonViewProfile : Profile
{
    protected override void Configure()
    {
        Mapper.CreateMap<PersonDTO, PersonViewModel>()
              .ForMember(model => model.Photos, opt => opt.MapFrom(dto => dto.Photos));
        Mapper.CreateMap<PersonViewModel, PersonDTO>()
              .ForMember(dto => dto.Photos, opt => opt.MapFrom(model => model.Photos));
    }
}

public class PersonShortViewProfile : Profile
{
    protected override void Configure()
    {
        Mapper.CreateMap<PersonDTO, PersonListViewModel.PersonShortViewModel>().IgnoreAllNonExisting();
        Mapper.CreateMap<IPagedList<PersonDTO>, IPagedList<PersonListViewModel.PersonShortViewModel>>()
              .AfterMap((s, d) =>
                        Mapper
                            .Map<IEnumerable<PersonDTO>, IEnumerable<PersonListViewModel.PersonShortViewModel>>(s, d))
              .ConvertUsing<PagedListConverter<PersonDTO, PersonListViewModel.PersonShortViewModel>>();
        Mapper.CreateMap<PersonDTO, PersonListViewModel.PersonShortViewModel>().IgnoreAllNonExisting();
    }
}
public class PhotoViewProfile : Profile
{
    protected override void Configure()
    {
        Mapper.CreateMap<PhotoDTO, PhotoViewModel>().ForSourceMember(dto => dto.PersonDTO, opt => opt.Ignore());
        Mapper.CreateMap<PhotoViewModel, PhotoDTO>().ForMember(dto => dto.PersonDTO, opt => opt.Ignore());
    }
}

Также у меня есть такие расширения для использования Expressions и преобразования в PagedList:

public class PagedListConverter<TIn, TOut> : ITypeConverter<IPagedList<TIn>, IPagedList<TOut>>
{
    /// <summary>
    /// Performs conversion from source to destination type
    /// </summary>
    /// <param name="context">Resolution context</param>
    /// <returns>
    /// Destination object
    /// </returns>
    public IPagedList<TOut> Convert(ResolutionContext context)
    {

        var source = (IPagedList<TIn>) context.SourceValue;
        var mapped = Mapper.Map<IList<TOut>>(source);
        return new StaticPagedList<TOut>(mapped,source.GetMetaData());
    }
}
    /// <summary>
/// An <see cref="ExpressionVisitor"/> implementation which uses <see href="http://automapper.org">AutoMapper</see> to remap property access from elements of type <typeparamref name="TSource"/> to elements of type <typeparamref name="TDestination"/>.
/// </summary>
/// <typeparam name="TSource">The type of the source element.</typeparam>
/// <typeparam name="TDestination">The type of the destination element.</typeparam>
public class AutoMapVisitor<TSource, TDestination> : ExpressionVisitor
{
    private readonly ParameterExpression _newParameter;
    private readonly TypeMap _typeMap = Mapper.FindTypeMapFor<TSource, TDestination>();

    /// <summary>
    /// Initialises a new instance of the <see cref="AutoMapVisitor{TSource, TDestination}"/> class.
    /// </summary>
    /// <param name="newParameter">The new <see cref="ParameterExpression"/> to access.</param>
    public AutoMapVisitor(ParameterExpression newParameter)
    {
        Contract.Requires(newParameter != null);

        _newParameter = newParameter;
        Contract.Assume(_typeMap != null);
    }

    [ContractInvariantMethod]
    [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Required for code contracts.")]
    private void ObjectInvariant()
    {
        Contract.Invariant(_typeMap != null);
        Contract.Invariant(_newParameter != null);
    }

    /// <summary>
    /// Visits the children of the <see cref="T:System.Linq.Expressions.MemberExpression"/>.
    /// </summary>
    /// <returns>
    /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
    /// </returns>
    /// <param name="node">The expression to visit.</param>
    protected override Expression VisitMember(MemberExpression node)
    {
        var propertyMaps = _typeMap.GetPropertyMaps();
        Contract.Assume(propertyMaps != null);

        // Find any mapping for this member
        var propertyMap = propertyMaps.SingleOrDefault(map => map.SourceMember == node.Member);
        if (propertyMap == null)
        {
            return base.VisitMember(node);
        }

        var destinationProperty = propertyMap.DestinationProperty;
        Contract.Assume(destinationProperty != null);

        var destinationMember = destinationProperty.MemberInfo;
        Contract.Assume(destinationMember != null);

        // Check the new member is a property too
        var property = destinationMember as PropertyInfo;
        if (property == null)
        {
            return base.VisitMember(node);    
        }

        // Access the new property
        var newPropertyAccess = Expression.Property(_newParameter, property);
        return base.VisitMember(newPropertyAccess);
    }
}

/// <summary>
/// A class which contains extension methods for <see cref="Expression"/> and <see cref="Expression{TDelegate}"/> instances.
/// </summary>
public static class ExpressionExtensions
{
    /// <summary>
    /// Remaps all property access from type <typeparamref name="TSource"/> to <typeparamref name="TDestination"/> in <paramref name="expression"/>.
    /// </summary>
    /// <typeparam name="TSource">The type of the source element.</typeparam>
    /// <typeparam name="TDestination">The type of the destination element.</typeparam>
    /// <typeparam name="TResult">The type of the result from the lambda expression.</typeparam>
    /// <param name="expression">The <see cref="Expression{TDelegate}"/> to remap the property access in.</param>
    /// <returns>An <see cref="Expression{TDelegate}"/> equivalent to <paramref name="expression"/>, but applying to elements of type <typeparamref name="TDestination"/> instead of <typeparamref name="TSource"/>.</returns>
    public static Expression<Func<TDestination, TResult>> RemapForType<TSource, TDestination, TResult>(
        this Expression<Func<TSource, TResult>> expression)
    {
        Contract.Requires(expression != null);
        Contract.Ensures(Contract.Result<Expression<Func<TDestination, TResult>>>() != null);

        var newParameter = Expression.Parameter(typeof (TDestination));
        Contract.Assume(newParameter != null);

        var visitor = new AutoMapVisitor<TSource, TDestination>(newParameter);
        var remappedBody = visitor.Visit(expression.Body);
        if (remappedBody == null)
        {
            throw new InvalidOperationException("Unable to remap expression");
        }
        return Expression.Lambda<Func<TDestination, TResult>>(remappedBody, newParameter);
    }
}

Используйте это так:

//...
 PersonListViewModel list = new PersonListViewModel();
            Mapper.CreateMap<PersonDTO, PersonListViewModel.PersonShortViewModel>().IgnoreAllNonExisting();
            if (predicate == null)
            {
                //Mapper.CreateMap<PersonDTO, PersonListViewModel.PersonShortViewModel>();
                list.Persons =
                    Mapper.Map<IPagedList<PersonListViewModel.PersonShortViewModel>>(
                        DataService.GetPersons()
                                   .OrderBy(person => person.Pib)
                                   .ToPagedList(pageIndex, pagingSettings.PageSize));
            }
            else
            {
                list.Persons =
                    Mapper.Map<IPagedList<PersonListViewModel.PersonShortViewModel>>(
                        DataService.GetPersonsBy(predicate.RemapForType<PersonViewModel, PersonDTO, bool>())
                                   .OrderBy(person => person.Pib)
                                   .ToPagedList(pageIndex, pagingSettings.PageSize));
            }
//...

Итак, это еще не все, но другой код, похоже, вообще не имеет значения. Но если это будет, пожалуйста, спросите меня об этом, и я также добавлю его сюда.

И создайте экземпляр конфигурации картографа здесь:

internal static class Program
{
    /// <summary>
    /// Main point.
    /// </summary>
    [STAThread]
    private static void Main()
    {
        AutoMapperBLLConfiguration.Configure();
        AutoMapperPLConfiguration.Configure();
        // etc.
    }
}

Мой главный вопрос: почему AutoMapper похоже на переписывание конфигурации, потому что после ее создания мне все равно нужно делать Mapper.CreateMap<> перед каждой операцией? И как мне создать разные конфиги для одинаковых сущностей в одном месте? Например. теперь он показывает эту ошибку:

Missing type map configuration or unsupported mapping.

Mapping types:
Person -> PersonDTO
Reestr.DAL.Entities.Person -> Reestr.BLL.DTO.PersonDTO

Destination path:
IEnumerable`1[0]

Source value:
System.Data.Entity.DynamicProxies.Person_81BD716087EE14CF5E255587795725BC7C06DC2382A1A8EBF33C29A04F551C34

Как я могу разделить AutoMapper конфигурацию создания между разными слоями (как вы видели, это была закомментированная строка в DataService конструкторе)? И не могли бы вы помочь мне с архитектурной логикой, потому что я только новичок, и я хочу, чтобы моя программа была лучшей практикой / я борюсь с этими проблемами в течение 3 дней... Спасибо!


person Dmitriy    schedule 20.10.2016    source источник
comment
Это большой кусок кода. У вас есть пример проекта, доступный где-нибудь?   -  person kalitsov    schedule 21.10.2016
comment
Каждый раз, когда вы вызываете инициализацию, вы перезаписываете конфигурацию: вызовите ее один раз и включите все соответствующие профили: чтобы Automapper отображал объекты, перемещающиеся между слоями, он должен знать обе стороны.   -  person stuartd    schedule 21.10.2016
comment
@stuartd, а как насчет того уровня представления, которому не нужно знать о логике на бизнес-уровне. Конечно, я могу поместить всю логику автомаппера на уровень представления, но разве это не должно быть неправильно?   -  person Dmitriy    schedule 24.10.2016


Ответы (1)


Инициализация в AutoMapper сбрасывает конфигурацию. Вы должны настроить AutoMapper один раз для каждого AppDomain с одним вызовом Initialize при запуске вашего приложения. Я использую экземпляры профиля, чтобы сохранить конфигурацию отдельно, чтобы вы могли использовать их для своих вызовов CreateMap. Я не знаю, что значит иметь слои, которые не «знают» друг о друге, но приложению необходимо знать все сборки для загрузки профилей. Просто вызовите Initialize, добавив все профили из вашего приложения.

Или обновитесь до AutoMapper 5, там есть метод загрузки профилей из сборок типов.

person Jimmy Bogard    schedule 23.10.2016
comment
Я понял тебя. И как видите я использую инициализацию через профили. К сожалению, мой проект должен быть включен. NET Framework 4.0, поэтому я могу использовать только automapper 4.1.1. Бизнес-уровень также должен инициализировать свою конфигурацию отображения, и я думаю, что уровень представления не должен знать об этом. - person Dmitriy; 24.10.2016
comment
Я до сих пор не знаю, что значит знать о слоях, это все одно приложение, поэтому все находится в одном и том же AppDomain. Код сканирования действительно тривиален, вы можете просто скопировать и вставить его самостоятельно. Это то, что мы сделали до того, как я это встроил. - person Jimmy Bogard; 24.10.2016
comment
Спасибо. Это хорошая идея, кажется, я хотел найти ее несколько дней назад. попробую ваш вариант) - person Dmitriy; 24.10.2016