1. 程式人生 > 實用技巧 >AutoMapper 簡單理解

AutoMapper 簡單理解

一、概要

  1、DTO?  

    DTO(Data Transfer Object)就是資料傳輸物件,說白了就是一個物件,只不過裡邊全是資料而已。

    為什麼要用DTO?

    (1)DTO更注重資料,對領域物件進行合理封裝,從而不會將領域物件的行為過分暴露給表現層

    (2)DTO是面向UI的需求而設計的,而領域模型是面向業務而設計的。因此DTO更適合於和表現層的互動,通過DTO我們實現了表現層與領域Model之間的解耦,因此改動領域Model不會影響UI層

    (3)DTO說白了就是資料而已,不包含任何的業務邏輯,屬於瘦身型的物件,使用時可以根據不同的UI需求進行靈活的運用 

    現在我們既然知道了使用DTO的好處,那麼我們肯定也想馬上使用它,但是這裡會牽扯一個問題:怎樣實現DTO和領域Model之間的轉換?  有兩個思路,我們要麼自己寫轉換程式碼,要麼使用工具。不過就應用而言,我還是覺得用工具比較簡單快捷,那就使用工具吧。其實這樣的轉換工具很多,不過我還是決定使用AutoMapper,因為它足夠輕量級,而且也非常流行,國外的大牛們都使用它。使用AutoMapper可以很方便的實現DTO和領域Model之間的轉換,它是一個強大的Object-Object Mapping工具。

  2、AutoMapper是什麼?

    AutoMapper是一個物件-物件對映器。

物件-物件對映通過將一種型別的輸入物件轉換為另一種型別的輸出物件來工作。使AutoMapper變得有趣的是,它提供了一些有趣的約定,以免去搞清楚如何將型別A對映為型別B。只要型別B遵循AutoMapper既定的約定,就需要幾乎零配置來對映兩個型別。

  3、為什麼使用AutoMapper?

    “為什麼使用物件-物件對映?”對映可以在應用程式中的許多地方發生,但主要發生在層之間的邊界中,例如,UI /域層之間或服務/域層之間。一層的關注點通常與另一層的關注點衝突,因此物件-物件對映導致分離的模型,其中每一層的關注點僅會影響該層中的型別。

二、AutoMapper簡單例項

  首先,在專案中引用NuGet包,右鍵依賴項,管理NuGet程式包,然後選擇瀏覽,搜尋AutoMapper,安裝;也可以在vs中使用開啟工具-庫程式包管理器-程式包管理控制平臺,輸入“Install-Package AutoMapper”命令

  1、第一種情況,兩個類欄位都一樣的情況

  (1)首先,建立一個model 

  public class Student
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public string Address { get; set; }
        public string Mobile { get; set; }
    }

  (2)建立一個需要對映的類  

    public class StudentDTO
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public string Address { get; set; }
        public string Mobile { get; set; }
    }

  (3)一個簡單的對映,由於Student和StudentDTO類欄位名稱一樣,型別相同.需要將Student類的物件對映到StudentDTO類的物件上面。需要對AutoMapper進行如下配置:

    //AutoMapper-10.0.0版本
    MapperConfiguration config = new MapperConfiguration
      (
           mp => mp.CreateMap<Student, StudentDTO>() //  給config進行配置對映規則                       
      );
      var mapConfig = config.CreateMapper();
      var studentDtoData = mapConfig.Map<List<StudentDTO>>(new List<Student> { new Student
      {
          ID = 1,
          Name = "hello",
          Address = "test",
          Mobile = "110"
      }});  //對映

  (4)完成程式碼

  //AutoMapper-10.0.0版本
  using AutoMapper;
  using System;
  using System.Collections.Generic;
  using System.Linq;

  namespace EFCoreDemo
  {
      class Program
      {
          static void Main(string[] args)
          {
              MapperConfiguration config = new MapperConfiguration
              (
                mp => mp.CreateMap<Student, StudentDTO>() //  給config進行配置對映規則                       
              );
              var mapConfig = config.CreateMapper();
              var studentDtoData = mapConfig.Map<List<StudentDTO>>(new List<Student> { new Student
              {
                  ID = 1,
                  Name = "hello",
                  Address = "test",
                  Mobile = "110"
              }});  //對映
              StudentDTO studentDTO = studentDtoData.FirstOrDefault();
              Console.WriteLine($"{studentDTO.ID},{studentDTO.Name},{studentDTO.Address},{studentDTO.Mobile}");
              Console.ReadKey();
          }
      }
      public class Student
      {
          public int ID { get; set; }
          public string Name { get; set; }
          public string Address { get; set; }
          public string Mobile { get; set; }
      }
      public class StudentDTO
      {
          public int ID { get; set; }
          public string Name { get; set; }
          public string Address { get; set; }
          public string Mobile { get; set; }
      }
  }

  執行結果如下:

        

  

  2、第二種對映情況:不同欄位名稱,甚至不同型別的兩個類如何對映呢,那就要手動的對映相應欄位了。 

       //AutoMapper-10.0.0版本
            MapperConfiguration config = new MapperConfiguration
            (
              mp => mp.CreateMap<Student, StudentDTO>() //  給config進行配置對映規則    
                      .ForMember(stdto => stdto._id, st => st.MapFrom(p => p.ID > 0 ? p.ID : -1))  // 指定對映欄位
                      .ForMember(stdto => stdto._name, st => st.MapFrom(p => p.Name))
                      .ForMember(stdto => stdto._address, st => st.MapFrom(p => p.Address))
                      .ForMember(stdto => stdto._mobile, st => st.MapFrom(p => p.Mobile))
            );
            var mapConfig = config.CreateMapper();
            var studentDtoData = mapConfig.Map<StudentDTO>(new Student
            {
                ID = 1,
                Name = "hello",
                Address = "test",
                Mobile = "110"
            });

  完整程式碼如下

using AutoMapper;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;

namespace EFCoreDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            //AutoMapper-10.0.0版本
            MapperConfiguration config = new MapperConfiguration
            (
              mp => mp.CreateMap<Student, StudentDTO>() //  給config進行配置對映規則    
                      .ForMember(stdto => stdto._id, st => st.MapFrom(p => p.ID > 0 ? p.ID : -1))  // 指定對映欄位
                      .ForMember(stdto => stdto._name, st => st.MapFrom(p => p.Name))
                      .ForMember(stdto => stdto._address, st => st.MapFrom(p => p.Address))
                      .ForMember(stdto => stdto._mobile, st => st.MapFrom(p => p.Mobile))
            );
            var mapConfig = config.CreateMapper();
            var studentDtoData = mapConfig.Map<StudentDTO>(new Student
            {
                ID = 1,
                Name = "hello",
                Address = "test",
                Mobile = "110"
            });
            Console.WriteLine($"{studentDtoData._id},{studentDtoData._name},{studentDtoData._address},{studentDtoData._mobile}");
            Console.ReadKey();
        }
    }
    public class Student
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public string Address { get; set; }
        public string Mobile { get; set; }
    }
    public class StudentDTO
    {
        public int _id { get; set; }
        public string _name { get; set; }
        public string _address { get; set; }
        public string _mobile { get; set; }
    }
}
View Code

  (4)對映List

  如何進行實體列表的對映呢,其實,配置上並沒有任何不同,只需要在使用上,換成list就可以了:

  List estu = new List();

  estu.Add(new Student

  {

  Name = "myname",

  Sex = 1,

  Age = "24",

  Birth = DateTime.Now

  });

  estu.Add(new Student

  {

  Name = "myname",

  Sex = 1,

  Age = "24",

  Birth = DateTime.Now

  });

  List<dto_student> slist = AutoMapper.Mapper.Map<list, List<dto_student>>(estu);

  複製程式碼

  (5)你可能覺得不太方便,那麼,我們可以將AutoMapper的Initialize放到應用開始的時候執行

  Mvc專案,可以放到程式的Global中,如果是.NET Core 2.0的MVC專案,可以放到StartUp中執行:

  這邊直接摘了前人的方法,有興趣可以看一下https://www.cnblogs.com/lvlinlv/p/7344916.html

  (6)那麼,你可以使用拓展的方法進行對映,這裡定義一個AutoMapperHelper操作類:

  using System;

  using System.Collections;

  using System.Collections.Generic;

  using System.Text;

  namespace AutoMapperTest

  {

  public static class AutoMapperHelper

  {

  public static T MapTo(this object obj)

  {

  if (obj == null) return default(T);

  return AutoMapper.Mapper.Map(obj);

  }

  public static List MapToList(this object source)

  {

  return AutoMapper.Mapper.Map<list>(source);

  }

  }

  }

  複製程式碼

  這樣,你就可以這麼使用:

  List estu = new List();

  estu.Add(new Student

  {

  Name = "myname",

  Sex = 1,

  Age = "24",

  Birth = DateTime.Now

  });

  estu.Add(new Student

  {

  Name = "myname",

  Sex = 1,

  Age = "24",

  Birth = DateTime.Now

  });

  var slist = estu.MapToList<dto_student>();

本文基於 AutoMapper 9.0.0

AutoMapper 是一個物件-物件對映器,可以將一個物件對映到另一個物件。

官網地址:http://automapper.org/

官方文件:https://docs.automapper.org/en/latest/

1 入門例子

public class Foo
{
    public int ID { get; set; }

    public string Name { get; set; }
}

public class FooDto
{
    public int ID { get; set; }

    public string Name { get; set; }
}

public void Map()
{
    var config = new MapperConfiguration(cfg => cfg.CreateMap<Foo, FooDto>());

    var mapper = config.CreateMapper();

    Foo foo = new Foo { ID = 1, Name = "Tom" };

    FooDto dto = mapper.Map<FooDto>(foo);
}

2 註冊

在使用Map方法之前,首先要告訴 AutoMapper 什麼類可以對映到什麼類。

var config = new MapperConfiguration(cfg => cfg.CreateMap<Foo, FooDto>());

每個 AppDomain 只能進行一次配置。這意味著放置配置程式碼的最佳位置是在應用程式啟動中,例如 ASP.NET 應用程式的 Global.asax 檔案。

從 9.0 開始Mapper.Initialize方法就不可用了。

2.1 Profile

Profile是組織對映的另一種方式。新建一個類,繼承Profile,並在建構函式中配置對映。

public class EmployeeProfile : Profile
{
    public EmployeeProfile()
    {
        CreateMap<Employee, EmployeeDto>();
    }
}

var config = new MapperConfiguration(cfg =>
{
    cfg.AddProfile<EmployeeProfile>();
});

Profile內部的配置僅適用於Profile內部的對映。應用於根配置的配置適用於所有建立的對映。

AutoMapper 也可以在指定的程式集中掃描從Profile繼承的類,並將其新增到配置中。

var config = new MapperConfiguration(cfg =>
{
    // 掃描當前程式集
    cfg.AddMaps(System.AppDomain.CurrentDomain.GetAssemblies());
    
    // 也可以傳程式集名稱(dll 名稱)
    cfg.AddMaps("LibCoreTest");
});

3 配置

3.1 命名約定

預設情況下,AutoMapper 基於相同的欄位名對映,並且是不區分大小寫的。

但有時,我們需要處理一些特殊的情況。

  • SourceMemberNamingConvention表示源型別命名規則
  • DestinationMemberNamingConvention表示目標型別命名規則

LowerUnderscoreNamingConventionPascalCaseNamingConvention是 AutoMapper 提供的兩個命名規則。前者命名是小寫幷包含下劃線,後者就是帕斯卡命名規則(每個單詞的首字母大寫)。

我的理解,如果源型別和目標型別分別採用了蛇形命名法駝峰命名法,那麼就需要指定命名規則,使其能正確對映。

public class Foo
{
    public int Id { get; set; }

    public string MyName { get; set; }
}

public class FooDto
{
    public int ID { get; set; }

    public string My_Name { get; set; }
}

public void Map()
{
    var config = new MapperConfiguration(cfg =>
    {
        cfg.CreateMap<Foo, FooDto>();

        cfg.SourceMemberNamingConvention = new PascalCaseNamingConvention();
        cfg.DestinationMemberNamingConvention = new LowerUnderscoreNamingConvention();
    });

    var mapper = config.CreateMapper();

    Foo foo = new Foo { Id = 2, MyName = "Tom" };

    FooDto dto = mapper.Map<FooDto>(foo);
}

3.2 配置可見性

預設情況下,AutoMapper 僅對映public成員,但其實它是可以對映到private屬性的。

var config = new MapperConfiguration(cfg =>
{
    cfg.ShouldMapProperty = p => p.GetMethod.IsPublic || p.SetMethod.IsPrivate;
    cfg.CreateMap<Source, Destination>();
});

需要注意的是,這裡屬性必須新增private set,省略set是不行的。

3.3 全域性屬性/欄位過濾

預設情況下,AutoMapper 嘗試對映每個公共屬性/欄位。以下配置將忽略欄位對映。

var config = new MapperConfiguration(cfg =>
{
	cfg.ShouldMapField = fi => false;
});

3.4 識別字首和字尾

var config = new MapperConfiguration(cfg =>
{
    cfg.RecognizePrefixes("My");
    cfg.RecognizePostfixes("My");
}

3.5 替換字元

var config = new MapperConfiguration(cfg =>
{
    cfg.ReplaceMemberName("Ä", "A");
});

這功能我們基本上用不上。

4 呼叫建構函式

有些類,屬性的set方法是私有的。

public class Commodity
{
    public string Name { get; set; }

    public int Price { get; set; }
}

public class CommodityDto
{
    public string Name { get; }

    public int Price { get; }

    public CommodityDto(string name, int price)
    {
        Name = name;
        Price = price * 2;
    }
}

AutoMapper 會自動找到相應的建構函式呼叫。如果在建構函式中對引數做一些改變的話,其改變會反應在對映結果中。如上例,對映後Price會乘 2。

禁用建構函式對映:

var config = new MapperConfiguration(cfg => cfg.DisableConstructorMapping());

禁用建構函式對映的話,目標類要有一個無參建構函式。

5 陣列和列表對映

陣列和列表的對映比較簡單,僅需配置元素型別,定義簡單型別如下:

public class Source
{
    public int Value { get; set; }
}

public class Destination
{
    public int Value { get; set; }
}

對映:

var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<Source, Destination>();
});
IMapper mapper = config.CreateMapper();

var sources = new[]
{
    new Source { Value = 5 },
    new Source { Value = 6 },
    new Source { Value = 7 }
};

IEnumerable<Destination> ienumerableDest = mapper.Map<Source[], IEnumerable<Destination>>(sources);
ICollection<Destination> icollectionDest = mapper.Map<Source[], ICollection<Destination>>(sources);
IList<Destination> ilistDest = mapper.Map<Source[], IList<Destination>>(sources);
List<Destination> listDest = mapper.Map<Source[], List<Destination>>(sources);
Destination[] arrayDest = mapper.Map<Source[], Destination[]>(sources);

具體來說,支援的源集合型別包括:

  • IEnumerable
  • IEnumerable
  • ICollection
  • ICollection
  • IList
  • IList
  • List
  • Arrays

對映到現有集合時,將首先清除目標集合。如果這不是你想要的,請檢視AutoMapper.Collection。

5.1 處理空集合

對映集合屬性時,如果源值為null,則 AutoMapper 會將目標欄位對映為空集合,而不是null。這與 Entity Framework 和 Framework Design Guidelines 的行為一致,認為 C# 引用,陣列,List,Collection,Dictionary 和 IEnumerables 永遠不應該為null

5.2 集合中的多型

這個官方的文件不是很好理解。我重新舉個例子。實體類如下:

public class Employee
{
    public int ID { get; set; }

    public string Name { get; set; }
}

public class Employee2 : Employee
{
    public string DeptName { get; set; }
}

public class EmployeeDto
{
    public int ID { get; set; }

    public string Name { get; set; }
}

public class EmployeeDto2 : EmployeeDto
{
    public string DeptName { get; set; }
}

陣列對映程式碼如下:

var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<Employee, EmployeeDto>().Include<Employee2, EmployeeDto2>();
    cfg.CreateMap<Employee2, EmployeeDto2>();
});
IMapper mapper = config.CreateMapper();

var employees = new[]
{
    new Employee { ID = 1, Name = "Tom" },
    new Employee2 { ID = 2, Name = "Jerry", DeptName = "R & D" }
};

var dto = mapper.Map<Employee[], EmployeeDto[]>(employees);

可以看到,對映後,dto中兩個元素的型別,一個是EmployeeDto,一個是EmployeeDto2,即實現了父類對映到父類,子類對映到子類。

如果去掉Include方法,則對映後dto中兩個元素的型別均為EmployeeDto

6 方法到屬性對映

AutoMapper 不僅能實現屬性到屬性對映,還可以實現方法到屬性的對映,並且不需要任何配置,方法名可以和屬性名一致,也可以帶有Get字首。

例如下例的Employee.GetFullName()方法,可以對映到EmployeeDto.FullName屬性。

public class Employee
{
    public int ID { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string GetFullName()
    {
        return $"{FirstName} {LastName}";
    }
}

public class EmployeeDto
{
    public int ID { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string FullName { get; set; }
}

7 自定義對映

當源型別與目標型別名稱不一致時,或者需要對源資料做一些轉換時,可以用自定義對映。

public class Employee
{
    public int ID { get; set; }

    public string Name { get; set; }

    public DateTime JoinTime { get; set; }
}

public class EmployeeDto
{
    public int EmployeeID { get; set; }

    public string EmployeeName { get; set; }

    public int JoinYear { get; set; }
}

如上例,IDEmployeeID屬性名不同,JoinTimeJoinYear不僅屬性名不同,屬性型別也不同。

var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<Employee, EmployeeDto>()
        .ForMember("EmployeeID", opt => opt.MapFrom(src => src.ID))
        .ForMember(dest => dest.EmployeeName, opt => opt.MapFrom(src => src.Name))
        .ForMember(dest => dest.JoinYear, opt => opt.MapFrom(src => src.JoinTime.Year));
});

8 扁平化對映

物件-物件對映的常見用法之一是將複雜的物件模型並將其展平為更簡單的模型。

public class Employee
{
    public int ID { get; set; }

    public string Name { get; set; }

    public Department Department { get; set; }
}

public class Department
{
    public int ID { get; set; }

    public string Name { get; set; }
}

public class EmployeeDto
{
    public int ID { get; set; }

    public string Name { get; set; }

    public int DepartmentID { get; set; }

    public string DepartmentName { get; set; }
}

如果目標型別上的屬性,與源型別的屬性、方法都對應不上,則 AutoMapper 會將目標成員名按駝峰法拆解成單個單詞,再進行匹配。例如上例中,EmployeeDto.DepartmentID就對應到了Employee.Department.ID

8.1 IncludeMembers

如果屬性命名不符合上述的規則,而是像下面這樣:

public class Employee
{
    public int ID { get; set; }

    public string Name { get; set; }

    public Department Department { get; set; }
}

public class Department
{
    public int DepartmentID { get; set; }

    public string DepartmentName { get; set; }
}

public class EmployeeDto
{
    public int ID { get; set; }

    public string Name { get; set; }

    public int DepartmentID { get; set; }

    public string DepartmentName { get; set; }
}

Department類中的屬性名,直接跟EmployeeDto類中的屬性名一致,則可以使用IncludeMembers方法指定。

var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<Employee, EmployeeDto>().IncludeMembers(e => e.Department);
    cfg.CreateMap<Department, EmployeeDto>();
});

9 巢狀對映

有時,我們可能不需要展平。看如下例子:

public class Employee
{
    public int ID { get; set; }

    public string Name { get; set; }

    public int Age { get; set; }

    public Department Department { get; set; }
}

public class Department
{
    public int ID { get; set; }

    public string Name { get; set; }

    public string Heads { get; set; }
}

public class EmployeeDto
{
    public int ID { get; set; }

    public string Name { get; set; }

    public DepartmentDto Department { get; set; }
}

public class DepartmentDto
{
    public int ID { get; set; }

    public string Name { get; set; }
}

我們要將Employee對映到EmployeeDto,並且將Department對映到DepartmentDto

var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<Employee, EmployeeDto>();
    cfg.CreateMap<Department, DepartmentDto>();
});