Should AutoMapper also be used as a Re-Shape mapping tool

泪湿孤枕 提交于 2019-12-08 05:06:37

问题


I know AutoMapper is just to map properties between 2 objects.

Automapper is not for reshaping data this must be programmed in my opinion.

I have a PeriodDTO.IEnumerable<Period> which needs to be reshaped into a PeriodListViewModel.List<CellViewModel>.

Each CellViewModel holds a List<RowViewModel>

This is no 1 to 1 mapping not and I guess maybe this should not be mapped - because its not possible -

public class PeriodRequest
    {
        public IEnumerable<Period> Periods { get; set; }
        public IEnumerable<DateTime> Weeks { get; set; }
    }

public class PeriodListViewModel
{       
    public PeriodListViewModel(IEnumerable<Period> periods)
    {
        CellViewModels = new List<CellViewModel>();
        var groupedPeriods = periods.GroupBy(p => p.LessonNumber).OrderBy(p => p.Key).ToList();
        foreach (var groupedPeriod in groupedPeriods)
        {
            var cellViewModel = new CellViewModel();
            CellViewModels.Add(cellViewModel);
            foreach (var period in groupedPeriod)
            {
                var rowViewModel = new RowViewModel();
                rowViewModel.Content = period.Content;
                rowViewModel.SchoolclassCode = period.SchoolclassCode;
                rowViewModel.DayName = period.LessonDate.ToShortDateString();
                rowViewModel.DayIndex = (int)period.LessonDate.DayOfWeek;
                rowViewModel.LessonNumber = period.LessonNumber;
                cellViewModel.Rows.Add(rowViewModel);
            }
        }
    }
    public List<CellViewModel> CellViewModels { get; set; }
}

public class CellViewModel
    {
        public CellViewModel()
        {
            Rows = new List<RowViewModel>();
        }
        public List<RowViewModel> Rows { get; set; }
    }

public class RowViewModel
    {
        public string DayName { get; set; }
        public string SchoolclassCode { get; set; }
        public string Content { get; set; }
        public int DayIndex { get; set; }
        public int LessonNumber { get; set; }
    }

public class Period
    {
        public int PeriodId { get; set; }
        public DateTime LessonDate { get; set; }
        public int LessonNumber { get; set; }
        public string SchoolclassCode { get; set; }
        public string Content { get; set; }
        public int SchoolyearId { get; set; }
        public Schoolyear Schoolyear { get; set; }
...
}

At the moment the PeriodListViewModel can be easily unit tested. I am asking myself now how can AutoMapper make the situation even better?


回答1:


While you don't have a trivial 1:1 Mapping from IEnumerable<Period> to List<CellViewModel>, you can still derive some value from Automapper by using it only at the place where you get class-to-class contact, which in your example is between Period and RowViewModel.

With this in mind, you can also make better use of LINQ and get everything done in one sweeping motion, provided you set up the projections that require re-shaping from Period to RowViewModel.

Here's a console code example that demonstrates exactly that. Note that we're using Automapper's notion of resolver classes via ValueResolve<TSource,TDestination> do the re-shaping, and I've added an extra constructor to CellViewModel to accept an IEnumerable<RowViewModel>, which helps improve our declarations.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using AutoMapper;

namespace GroupingMapping
{
    public class PeriodRequest
    {
        public IEnumerable<Period> Periods { get; set; }
        public IEnumerable<DateTime> Weeks { get; set; }
    }

    public class RowViewModel
    {
        public string DayName { get; set; }
        public string SchoolclassCode { get; set; }
        public string Content { get; set; }
        public int DayIndex { get; set; }
        public int LessonNumber { get; set; }
    }

    public class Period
    {
        public int PeriodId { get; set; }
        public DateTime LessonDate { get; set; }
        public int LessonNumber { get; set; }
        public string SchoolclassCode { get; set; }
        public string Content { get; set; }
        public int SchoolyearId { get; set; }
        // No definition provided for Schoolyear
        //public Schoolyear Schoolyear { get; set; }
    }

    public class CellViewModel
    {
        public CellViewModel()
        {
            Rows = new List<RowViewModel>();
        }

        public CellViewModel(IEnumerable<RowViewModel> rowVMSet)
        {
            Rows = new List<RowViewModel>(rowVMSet);
        }

        public List<RowViewModel> Rows { get; set; }
    }

    public class PeriodListViewModelEx
    {
        public PeriodListViewModelEx(IEnumerable<Period> periods)
        {
            CellViewModels = new List<CellViewModel>(periods
                .GroupBy(p => p.LessonNumber)
                .OrderBy(grp => grp.Key)
                .Select(grp =>
                {
                    return new CellViewModel(
                        grp.Select(p => { return Mapper.Map<Period, RowViewModel>(p); }));
                }));
        }
        public List<CellViewModel> CellViewModels { get; set; }
    }

    class DateTimeToDateNameResolver : ValueResolver<DateTime, string>
    {
        protected override string ResolveCore(DateTime source)
        {
            return source.ToShortDateString();
        }
    }

    class DateTimeToDayOfWeekResolver : ValueResolver<DateTime, int>
    {
        protected override int ResolveCore(DateTime source)
        {
            return (int)source.DayOfWeek;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Mapper.CreateMap<Period, RowViewModel>()
                .ForMember(dest => dest.DayName, opt => opt.ResolveUsing<DateTimeToDateNameResolver>().FromMember(src => src.LessonDate))
                .ForMember(dest => dest.DayIndex, opt => opt.ResolveUsing<DateTimeToDayOfWeekResolver>().FromMember(src => src.LessonDate));

            Period[] periods = new Period[3];

            periods[0] = new Period { PeriodId = 1, LessonDate = DateTime.Today.Add(new TimeSpan(1, 0, 0, 0)), LessonNumber = 101, SchoolclassCode = "CS101", Content = "Intro to CS", SchoolyearId = 2013 };
            periods[1] = new Period { PeriodId = 2, LessonDate = DateTime.Today.Add(new TimeSpan(2, 0, 0, 0)), LessonNumber = 101, SchoolclassCode = "CS101", Content = "Intro to CS", SchoolyearId = 2013 };
            periods[2] = new Period { PeriodId = 3, LessonDate = DateTime.Today.Add(new TimeSpan(1, 0, 0, 0)), LessonNumber = 102, SchoolclassCode = "EN101", Content = "English (I)", SchoolyearId = 2013 };

            PeriodListViewModelEx pvModel = new PeriodListViewModelEx(periods);

            Console.WriteLine("CellViews: {0}", pvModel.CellViewModels.Count);

            foreach (CellViewModel cvm in pvModel.CellViewModels)
            {
                Console.WriteLine("{0} items in CellViewModel Group", cvm.Rows.Count);
            }

            Console.WriteLine("Inspecting CellViewModel Rows");
            foreach (CellViewModel cvm in pvModel.CellViewModels)
            {
                foreach (RowViewModel rvm in cvm.Rows)
                {
                    Console.WriteLine("  DayName: {0}", rvm.DayName);
                    Console.WriteLine("  SchoolclassCode: {0}", rvm.SchoolclassCode);
                    Console.WriteLine("  Content: {0}", rvm.Content);
                    Console.WriteLine("  DayIndex: {0}", rvm.DayIndex);
                    Console.WriteLine("  LessonNumber: {0}", rvm.LessonNumber);
                    Console.WriteLine("  -");
                }
                Console.WriteLine("--");
            }

            Console.ReadKey();
        }
    }
}



回答2:


For a few months I primarily used AutoMapper for creating my view models from my entity models, basically what you are doing there. I have used AutoMapper to flatten and unflatten very complex models.

I personally wouldn't do it in a constructor of a view model though. The reason being is that you might have different sources of a PeriodListViewModel. The idea is that once you have your mappers created/configured, at will you can do Mapper.Map<PeriodViewModel(collectionOfCells); or Mapper.Map<PeriodViewModel(periodEntity); depending on what the situation calls for. This way neither your PeriodViewModels nor your Period class have any knowledge or relation to each other. If the Period class changes in response to database changes, then you just tweak your automapper mappings to accomodate the change, leaving PeriodViewModel class unchanged, and thus not having to touch any of the views which use it. Vice versa as well.

Even if I wasn't using automapper, I'd still have static helper methods do the copying of entity models to view models. Something as simple as a View Model should have a simple default constructor to make it easy to use.

However, I will say that doing complex mappings in AutoMapper is quite challenging, and debugging sometimes daunting. Many times I found myself in another developer's office helping them understand the chain of mappings that was occuring. As it's name implies, as it delves into child objects/collections/navigation properties, it automatically maps them, and thus alot of things are happening implicitely, which is really difficult to debug. You basically have to do alot of mental visualization of these object graphs and then go hunt down the mapping that you think is being used. Some of the mapping options don't allow multi-line anonymous methods, and thus you can't put a breakpoint or extra logging code.

As a developer often pushing data from the database to the frontend, AutoMapper is pretty much exactly what I had always dreamed of, but after using it for a few months, I probably would avoid using it to translate object graphs more than 2 levels deep. For what you're doing in your example, I think AutoMapper would be fine.

Update:

I would keep querying and mapping seperate. First query to get back my entities, because I might need to pass in parameters to where criteria as well. Then the resulting List from ToList is passed to automapper.map to convert the Period entities to PeriodViewModel's. In your case you have a nested collection because of the group by. You could either create a mapping from a collection of Periods(groupedPeriod) to a single CellViewModel, then

foreach (var groupOfPeriods in groupedPeriods)
{
            Mapper.Map<CellViewModel>(groupOfPeriods );
            CellViewModels.Add(cellViewModel);
}

I believe this will work(given you create a mapping for it, because an IGrouping implement IEnumerable and in this case TElement is a Period.

The other option, is instead of creating a mapping from IEnumerable to CellViewModel, is to isntead create a mapping from Period to RowViewModel and do this:

...
foreach (var groupedPeriod in groupedPeriods)
{
    var cellViewModel = new CellViewModel();
    CellViewModels.Add(cellViewModel);
    foreach (var period in groupedPeriod)
    {
        var rowViewModel = Mapper.Map<RowViewModel>(period); 
        cellViewModel.Rows.Add(rowViewModel);    
    }
}
...

That second option would probably be the simpler option, as the mapping will be simpler. I think the first option might be more difficult to maintain/debug, since too much "magic" would be baked into the CreateMap.

I don't have any of my old auto mapper code handy though, so I can't give you accurate code examples on how to configure the actual CreateMap parts, but the second option should be fairly straightforward.

Also, since automapper understands how to map lists of things just by you telling it how to map a single item, you could probably get away with this:

...
foreach (var groupedPeriod in groupedPeriods)
{
    var cellViewModel = new CellViewModel();
    CellViewModels.Add(cellViewModel);
    cellViewModel.Rows = Mapper.Map<List<RowViewModel>>(groupedPeriod);
}
...

It depends on exactly what the type of the collection Rows is, if it is not a List<RowViewModel> then change that part so that AutoMapper creates the correct type of collection. See for more information on mapping lists: https://github.com/AutoMapper/AutoMapper/wiki/Lists-and-arrays



来源:https://stackoverflow.com/questions/15122485/should-automapper-also-be-used-as-a-re-shape-mapping-tool

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!