【问题标题】:Should AutoMapper also be used as a Re-Shape mapping toolAutoMapper 是否也可以用作 Re-Shape 映射工具
【发布时间】:2013-02-27 21:10:20
【问题描述】:

我知道 AutoMapper 只是在 2 个对象之间映射属性。

Automapper 不适用于重塑数据,我认为这必须编程。

我有一个PeriodDTO.IEnumerable<Period>,需要将其改造成PeriodListViewModel.List<CellViewModel>

每个CellViewModel 都有一个List<RowViewModel>

这不是一对一的映射,我想也许这不应该被映射 - 因为它不可能 -

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

目前 PeriodListViewModel 可以很容易地进行单元测试。我现在问自己 AutoMapper 如何才能让情况变得更好?

【问题讨论】:

  • 您的意思是“ReSharper”还是“Re-Shape”?
  • 我认为“重塑者”的意思是重塑。我也把它读作 ReSharper。
  • 更改了标题,现在改名为 RE-SHAPE :)

标签: c# viewmodel automapper dto


【解决方案1】:

虽然您没有从 IEnumerable&lt;Period&gt;List&lt;CellViewModel&gt; 的微不足道的 1:1 映射,但您仍然可以从 Automapper 中获得一些价值,方法是仅在您获得班级联系的地方使用它,这在您的示例中,介于 PeriodRowViewModel 之间。

考虑到这一点,您还可以更好地利用 LINQ,并在一个全面的动作中完成所有工作,前提是您将需要从 Period 重新整形的投影设置为 RowViewModel

这是一个控制台代码示例,可以准确地说明这一点。请注意,我们通过 ValueResolve&lt;TSource,TDestination&gt; 使用 Automapper 的解析器类概念进行重新整形,并且我在 CellViewModel 中添加了一个额外的构造函数以接受 IEnumerable&lt;RowViewModel&gt;,这有助于改进我们的声明。

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 个问题:1.) 为什么 ValueResolver 类对我来说似乎是开销。在我的 PeriodProfile.cs 类中,我可以通过将这些属性映射到 (int)Period.LessonDate.DayOfWeek 轻松地将 Period.LessonDate 映射到 RowViewModel.DayIndex。对于 ToShortDateString,仅使用 ToShortDateString 即可实现相同的 1:1 映射。我认为不需要 ValueResolver。所以你改变的是“只是”把一个 Period/RowViewModel 映射到我的 PeriodListViewModel 中。我宁愿考虑将所有逻辑外包到配置文件类中,但这是不可能的?
  • 这不是解决方案的一部分,但我真的很感兴趣,因为您已经非常了解这个问题,您将如何处理相反的方式?从客户端到服务器发送已经分组的 PeriodListViewModel 并将其映射到 IEnumerable?在那里,我无法将解组/展平代码放入 PeriodListViewModel 的构造函数中。这个逻辑在哪里?
  • ValueResolver 类有点开销;但是,如果您使用的是 Automapper,那应该已经是您愿意做出的权衡,以希望花费更少的时间编写和验证代码。此外,虽然您可以在类关系的基础上手动编写适配器,但使用 Automapper 的工具可以让您在需要投影时只编写一行映射代码。由于编程中有很多事情,您可以通过多种方式执行相同的操作;但 Automapper 的主要优势是在执行投影时能够简洁,但代价是准备时冗长。
  • 反之,您可以编写一个包含IEnumerable&lt;Period&gt; 的模型,该模型可以从PeriodListViewModelIEnumerable&lt;CellViewModel&gt; 实例化(注意List&lt;&gt; 可以被视为IList&lt;&gt;源自IEnumerable&lt;&gt;)。但是,一个问题是您的RowViewModelPeriod 没有密切的对应关系。如果这不是问题(也许您只打算从一组RowViewModels 中的部分信息中创建新的Period 项目),您仍然可以使用 Automapper 作为一种将数据投射回来的方法,只需设置一个反向映射你认为合适的。
  • 如果你还记得我在初始化问题中的最后一句话:“目前 PeriodListViewModel 可以很容易地进行单元测试”。我认为现在情​​况变得更糟了。我使用 MOQ 进行不能模拟静态类/方法的模拟。这意味着我无法使用您的解决方案对我的 PeriodListViewModel 进行单元测试,除了来自 Automapper 的映射周期过程之外,其余部分除外。通常我使用 Automapper 中的 IMappingEngine 可以很容易地模拟但我必须将 imappingengine 实例注入我的 PeriodListViewModel 嗯......再次努力......
【解决方案2】:

几个月来,我主要使用 AutoMapper 从我的实体模型创建视图模型,基本上就是你在那里所做的。我使用 AutoMapper 对非常复杂的模型进行展平和展开。

我个人不会在视图模型的构造函数中这样做。原因是您可能有不同的 PeriodListViewModel 来源。这个想法是,一旦您创建/配置了映射器,您就可以根据情况要求随意执行Mapper.Map&lt;PeriodViewModel(collectionOfCells);Mapper.Map&lt;PeriodViewModel(periodEntity);这样,您的 PeriodViewModels 和 Period 类都没有任何知识或相互关系。 如果 Period 类因数据库更改而更改,那么您只需调整自动映射器映射以适应更改,留下 PeriodViewModel类不变,因此不必触及任何使用它的视图。反之亦然。

即使我没有使用自动映射器,我仍然可以使用静态辅助方法将实体模型复制到视图模型。像 View Model 这样简单的东西应该有一个简单的默认构造函数以使其易于使用。

但是,我要说的是,在 AutoMapper 中进行复杂的映射非常具有挑战性,而且调试有时令人生畏。很多时候,我发现自己在另一个开发人员的办公室帮助他们了解正在发生的映射链。顾名思义,当它深入研究子对象/集合/导航属性时,它会自动映射它们,因此隐式发生了很多事情,这真的很难调试。您基本上必须对这些对象图进行大量的心理可视化,然后去寻找您认为正在使用的映射。一些映射选项不允许多行匿名方法,因此您不能放置断点或额外的日志记录代码。

作为一个经常将数据从数据库推送到前端的开发者,AutoMapper 几乎是我一直梦寐以求的,但使用了几个月后,我可能会避免使用它来翻译超过 2 个对象图层次深。对于您在示例中所做的事情,我认为 AutoMapper 会很好。

更新:

我会继续单独查询和映射。第一个查询以取回我的实体,因为我可能还需要将参数传递给 where 条件。然后将来自 ToList 的结果列表传递给 automapper.map 以将 Period 实体转换为 PeriodViewModel。在您的情况下,由于 group by,您有一个嵌套集合。您可以创建从 Periods(groupedPeriod) 集合到单个 CellViewModel 的映射,然后

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

我相信这会起作用(假设您为它创建了一个映射,因为 IGrouping 实现了 IEnumerable,在这种情况下 TElement 是一个 Period。

另一种选择不是创建从 IEnumerable 到 CellViewModel 的映射,而是创建从 Period 到 RowViewModel 的映射并执行以下操作:

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

第二个选项可能更简单,因为映射会更简单。我认为第一个选项可能更难以维护/调试,因为 CreateMap 中会包含太多“魔法”。

虽然我手头没有任何旧的自动映射器代码,所以我无法为您提供有关如何配置实际 CreateMap 部件的准确代码示例,但第二个选项应该相当简单。

此外,由于 automapper 了解如何仅通过您告诉它如何映射单个项目来映射事物列表,因此您可能会侥幸成功:

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

这取决于集合Rows 的确切类型,如果不是List&lt;RowViewModel&gt;,则更改该部分以便AutoMapper 创建正确的集合类型。有关映射列表的更多信息,请参阅:https://github.com/AutoMapper/AutoMapper/wiki/Lists-and-arrays

【讨论】:

  • 我无法理解如何将 Linq groupby/order 内容包含在 AutoMapper Profile 类中,或者我是否走错了路,Linq 内容应该保留在 PeriodListViewModel 中。如果它可以轻松编程,它很难找到如何映射它的方法......我无法想象它是如何工作的。您能否继续使用一个不只是粗略提示的小代码示例?您上面的描述 Mapper PeriodViewModel(collectionOfCell) 实际上应该是 Mapper 一个周期有一个单元格,但是一个 CellVM 没有一个周期的属性...??!!
  • 因此我无法想象我该如何映射它。就我而言,ctor 注射完全没问题。 PeriodListViewModel 的不同来源?请您进一步解释一下吗?
  • 我在答案中添加了更多细节和示例。我该回去工作了:)祝你好运。
  • 感谢您的时间和努力。正如我告诉 meklarian 而不是字面上的那样,但是当我编写双重代码时,自动映射器涵盖了所有内容,然后为了 KISS,我坚持使用我自己以前的可测试解决方案 :)
猜你喜欢
  • 1970-01-01
  • 2012-08-31
  • 1970-01-01
  • 2011-05-15
  • 1970-01-01
  • 2011-01-05
  • 1970-01-01
  • 1970-01-01
  • 2011-12-08
相关资源
最近更新 更多