ASP.NET CoreでAutoMapperを使う

AutoMapperも既に.Net Coreへの対応が行われております。
ということで、ASP.NET CoreでAutoMapperを動かしてみたいと思います。

テスト環境

テスト環境はMacで、dotnet --info の結果は以下の通りです。

ryuichi:coreMvcAutoMapper daigo$ dotnet --info
.NET Command Line Tools (1.0.0-preview2-1-003177)

Product Information:
 Version:            1.0.0-preview2-1-003177
 Commit SHA-1 hash:  a2df9c2576

Runtime Environment:
 OS Name:     Mac OS X
 OS Version:  10.12
 OS Platform: Darwin
 RID:         osx.10.12-x64

そもそもAutoMapperとは

一応、おさらい。
その名前の通り「自動でマップしてくれる人」です。
プログラム的な表現としては、「異なるオブジェク間のマッピングを行ってくれるライブラリ」です。
オブジェクトのマッピングは、いつ必要になるか?それは レイヤー間でオブジェクトを受け渡す際です。レイヤー間とは「同一プロセス内のクラスメソッド間」の場合もあれば、「プロセスをまたぐ」場合、「ネットワークをまたぐ」場合もあります。

1つの例として、「SPA(Single Page Application)において、WebAPI呼び出しで画面表示データを取得するケース」を考えます。
サーバーサイドの処理としては「オブジェクトのリレーション関係がドメイン領域を適切に表す構造」であることが望まれるかもしれませんが、ブラウザクライアントのUIにおいては「画面表示に必要な項目のみを含んだフラットなデータ構造」が望まれる場合があります。

つまり、改めて言い換えると、ビジネスロジックにおいては、問題領域を扱う為のドメインクラスの形でモデルクラスを保持する事が適切な場合でも、それを画面に表示するView領域では、ドメインクラスを画面表示に最適化したシンプルなDTOである方が適切なケースがあります。

AutoMapperを使う

では具体的なAutoMapperの使い方の説明に入ります。
ここでは、ASP.NET Core MVCの基本プロジェクトが構成されている事を前提とします。

Model / DTOの準備

サンプルとして書籍販売サイトで、販売中の書籍一覧を表示するようなケースを想定します。
以下のような構造の「Bookクラス」を「BookDtoクラス」にマップすることを想定します。

f:id:daigo-knowlbo:20161117231151p:plain

Bookクラスはメンバープロパティとして「Authorオブジェクト」「Publisher」オブジェクトを保持します。
それに対してBookDtoクラスは画面表示に必要な項目をフラットに保持しています。

具体的な実装は以下の通りです。

// Book.cs
namespace CoreMvcAutoMapper.Models
{
  public class Book {
    public int Id { get; set; }
    public string Name { get; set; }
    public string ISBN { get; set; }
    public Author Author { get; set;}
    public Publisher Publisher { get; set; }
  }
}
// Author.cs
namespace CoreMvcAutoMapper.Models
{
  public class Author {
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
  }
}
// Publisher.cs
namespace CoreMvcAutoMapper.Models
{
  public class Publisher {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string Phone { get; set; }
  }
}
// BookDto.cs
namespace CoreMvcAutoMapper.Models.Dto
{
  public class BookDto {
    public int BookId { get; set; }
    public string BookName { get; set; }
    public string ISBN { get; set; }
    public string AuthorName { get; set; }
    public string PublisherName { get; set; }
  }
}

Dependenciesの追記

まず Project.json に対して AutoMapper 関連のライブラリ参照を定義します。
以下が Project.json の抜粋ですが、 "AutoMapper" / "AutoMapper.Extensions.Microsoft.DependencyInjection" の2つライブラリへの参照を追加しています。

{
  ...省略...

  "dependencies": {
  "Microsoft.NETCore.App": {
    "version": "1.1.0",
    "type": "platform"
  },
  "Microsoft.AspNetCore.Mvc": "1.0.1",
  "Microsoft.AspNetCore.Razor.Tools": {
    "version": "1.0.0-preview2-final",
    "type": "build"
  },
  "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
  "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",

  "AutoMapper": "5.1.1",
  "AutoMapper.Extensions.Microsoft.DependencyInjection": "1.1.2"

  },

  ...省略...
}

Profileの準備

Profileクラスを用意します。
Profileクラスでは、オブジェクト間のマッピング方法の定義を行います。
今回の例では Bookクラス を BookDtoクラス にどのようにマップするかという定義を行います。
以下が実装になります。

// AutoMapperProfileConfiguration.cs
using AutoMapper;

using CoreMvcAutoMapper.Models;
using CoreMvcAutoMapper.Models.Dto;

namespace CoreMvcAutoMapper
{
  public class AutoMapperProfileConfiguration : Profile
  {
    public AutoMapperProfileConfiguration() 
    {
      CreateMap<Book, BookDto>()
        .ForMember( dst => dst.BookId, src => src.MapFrom(s => s.Id))
        .ForMember( dst => dst.BookName, src => src.MapFrom(s => s.Name))
        .ForMember( dst => dst.AuthorName, src => src.MapFrom(s => s.Author.LastName + s.Author.FirstName));
    }
  }
}

定義するプロファイルクラスは、AutoMapper.Profileクラスを継承します。AutoMapperProfileConfigurationというクラス名は任意の名前でOKです。
コンストラクタ内で「CreateMap<T1, T2>()メソッド」を呼び出しています。これによって「クラス間のマッピング定義」をAutoMapperに対して宣言することになります。
T1が変換元クラス名、T2が変換先クラス名、となります。
ForMember()定義が3つ並んでします。それぞれ、「変換先オブエジェクトの対象プロパティに対して、変換元のどのプロパティを割り当てるか」を宣言しています。
つまり、「Book.Id は BookDto.BookId にマップ / Book.Name は BookDto.BookName にマップ」 となります。
3つ目のForMember()は、Book.Authorオブジェクトでは「FirstName」と「LastName」に分かれていた著者名を連結して、BookDto.AuthorNameプロパティにマップすることを定義しています。
さらにもう1つ「BookDtoクラスには ISBN プロパティ」が存在しますが、これについて ForMember() 定義がなされていません。しかし、この後のプログラムを実行すると、きちんと「Book.ISBN → BookDto.ISBN」のマップが行われます。これは「変換元と変換先でプロパティ名が同一の場合、自動的にマップが行われる」というAutoMapperの仕様が働くためです。

Mapperの構成初期化とDI登録

Profileはマップの定義であり、具体的なマップ処理を実行(プログラムからキック)するのは「IMapperインターフェイス」オブジェクト経由での操作になります。
今回はASP.NET Core MVCから AutoMapper を利用する為、APS.NET Core MVC の DI(Dependency Injection) の仕組みに従って「IMapperオブジェクトをコントローラに対して、コンストラクタ インジェクション」する事とします。
この初期化処理は Startup.cs で行います。

// Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

using AutoMapper;

namespace CoreMvcAutoMapper
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
        }
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            // AutoMapperをプロファイルを元に初期化してサービスに登録
            services.AddAutoMapper(cfg =>{
                cfg.AddProfile<AutoMapperProfileConfiguration>();
            });
            // IMapper型に対してMapperオブジェクトをsingletonでDIする様に定義を追加
            services.AddSingleton<IMapper, Mapper>();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

上記は最低限のStartup実装としています。通常の ASP.NET Core MVC プロジェクトではロガーの初期化、構成ファイルの読み取り、DIへの各種サービスの追加等々、各処理が追記されるはずです。
ポイントは ConfigureServices(IServiceCollection services) において以下の2つの処理を行うことにあります。

  • 「services.AddAutoMapper()」:Profileを元にAutoMapperの初期化とサービスへの追加を行う。
  • 「services.AddSingleton()」:IMapper型オブジェクトが求められた際に Mapperオブジェクトをインジェクションする様にDIサービスに追加する。

Map処理で型コンバート

「Book→BookDto変換を実装したMapperオブジェクト」を、コントローラクラスでコンストラクタインジェクションにより受け取り、利用したいと思います。
以下は BookControllerクラス の定義であり、「List()メソッド」はBookDtoリストをJSON形式で返却するWebAPIの実装になります。

// BookController.cs
using System.Collections.Generic;

using Microsoft.AspNetCore.Mvc;

using AutoMapper;

using CoreMvcAutoMapper.Models;
using CoreMvcAutoMapper.Models.Dto;

namespace CoreMvcAutoMapper.Controllers
{
  public class BookController : Controller
  {
    private readonly IMapper _mapper;
        
    public BookController(IMapper mapper) { // ← Mapperオブジェクトがインジェクションされる
      // 自分のプロパティに保持
      this._mapper = mapper;
    }

    /*
     * 書籍一覧をJSON形式で取得するWebAPIです。
     */
    public IActionResult List()
    {
      // サンプルのBookオブジェクトリストを取得(実際にはサービス層やDAO層経由で取得)
      List<Book> books = CreateBooks();
      // マップ処理実行!
      List<BookDto> bookDtos = this._mapper.Map<List<Book>, List<BookDto>>(books);
      
      return Ok(bookDtos);
    }

    /*
     * サンプルデータを作成します。
     */
    private List<Book> CreateBooks()
    {
      string[] names = {"C#入門", "Xamarin Form活用", "Visual Studio 2017活用術", "Essential .NET Core", "Java詳説", "実践Docker", "UWP入門", "Professional Azure", "Swift開発", "OSSの世界"};
      string[] publisherNames = {"技術評論社", "翔泳社", "光和コンピュータ"};

      List<Book> books = new List<Book>();

      for(int i=0 ; i<10 ; i++ )
      {
        Book book = new Book() {
          Id = i,
          Name = names[i],
          ISBN = "ISBN-" + i.ToString(),
          Author = new Author() {
            Id = i*10,
            FirstName = "FirstName " + i.ToString(),
            LastName = "LastName " + i.ToString()
          },
          Publisher = new Publisher() { 
            Id = i * 15,
            Name = publisherNames[i%3],
            Address = "Address " + i.ToString() + " - " + i.ToString(),
            Phone = "0120-" + i.ToString() + "-2351"
          }
        };
        books.Add(book);
      }

      return books;
    }
  }
}

コンストラクタの引数としてIMapper型オブジェクトが定義されています。ASP.NET Core MVCランタイム環境はStartupで定義されたサービスへのDIオブジェクト登録に従って IMapper 型に合致するオブジェクト(この場合は、Book→BookDto変換を備えたMapperオブジェクト)を自動的にインジェクションします。 コンストラクタ引数で得られた Mapperオブジェクトは、別のメソッドで利用したい為、privateフィールドメンバー「IMapper _mapper」に保持します。
「List()メソッド」は 、URL「/Book/List」にルーティングされるWebAPIインターフェイスです。
まず「CreateBooks()」で、Bookオブジェクトのリストを取得します。これはサンプルとして固定のBookリストを作成するテスト用メソッドとして用意したものです。
次に以下のMasp()メソッド呼び出しをすることで List → List の変換を行なっています。

List<BookDto> bookDtos = this._mapper.Map<List<Book>, List<BookDto>>(books);

ここではList → Listの変換を行いましたが、単一オブジェクトの変換の場合は以下の様な実装となります。

BookDto bookDto = this._mapper.Map(Book, BookDto>(book);

/Book/List で得られるJSONデータ

上記実装を行なったASP.NET Core MVCアプリケーションを dotnet restore → dotnet run して /Book/List にアクセスしますと、以下のJSONデータが得られます。
各プロパティが正しく変換されたことを確認できます。

[
    {
        "bookId": 0,
        "bookName": "C#入門",
        "isbn": "ISBN-0",
        "authorName": "LastName 0FirstName 0",
        "publisherName": "技術評論社"
    },
    {
        "bookId": 1,
        "bookName": "Xamarin Form活用",
        "isbn": "ISBN-1",
        "authorName": "LastName 1FirstName 1",
        "publisherName": "翔泳社"
    },
    {
        "bookId": 2,
        "bookName": "Visual Studio 2017活用術",
        "isbn": "ISBN-2",
        "authorName": "LastName 2FirstName 2",
        "publisherName": "光和コンピュータ"
    },
    {
        "bookId": 3,
        "bookName": "Essential .NET Core",
        "isbn": "ISBN-3",
        "authorName": "LastName 3FirstName 3",
        "publisherName": "技術評論社"
    },
    {
        "bookId": 4,
        "bookName": "Java詳説",
        "isbn": "ISBN-4",
        "authorName": "LastName 4FirstName 4",
        "publisherName": "翔泳社"
    },
    {
        "bookId": 5,
        "bookName": "実践Docker",
        "isbn": "ISBN-5",
        "authorName": "LastName 5FirstName 5",
        "publisherName": "光和コンピュータ"
    },
    {
        "bookId": 6,
        "bookName": "UWP入門",
        "isbn": "ISBN-6",
        "authorName": "LastName 6FirstName 6",
        "publisherName": "技術評論社"
    },
    {
        "bookId": 7,
        "bookName": "Professional Azure",
        "isbn": "ISBN-7",
        "authorName": "LastName 7FirstName 7",
        "publisherName": "翔泳社"
    },
    {
        "bookId": 8,
        "bookName": "Swift開発",
        "isbn": "ISBN-8",
        "authorName": "LastName 8FirstName 8",
        "publisherName": "光和コンピュータ"
    },
    {
        "bookId": 9,
        "bookName": "OSSの世界",
        "isbn": "ISBN-9",
        "authorName": "LastName 9FirstName 9",
        "publisherName": "技術評論社"
    }
]

AutoMapper参考リンク

「本家」 automapper.org

「本家GitHubgithub.com

「Jimmy BogardのBlog 」
Jimmy Bogard's Blog | Strong opinions, weakly held