Entity Framework Core 1.1のHasField()とUsePropertyAccessMode()を使ってみた

Entity Framework Core 1.0 → 1.1における機能追加の1つとして「HasField()メソッドの追加」というものがあります。
Entity Frameworkでは、基本的に
「モデルクラス=データベース上のテーブル」
「モデルクラスのプロパティ=データベーステーブル上のカラム」
というマッピングを行います。
HasField()を利用すると「(クラス)フィールド」をデータベーステーブルカラムにマップすることが出来ます。

テスト環境

本投稿のテスト環境は以下の通りです。

C:\Users\ryuichi>dotnet --info
.NET Command Line Tools (1.0.0-preview4-004110)

Product Information:
 Version:            1.0.0-preview4-004110
 Commit SHA-1 hash:  740a7fe3fd

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.14393
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\1.0.0-preview4-004110

.NET Core Downloadsからダウンロードできるstableなバージョンではなく、デイリービルドされているGithub上のhttps://github.com/dotnet/cliから2016/11/23にダウンロードを行いました。
このバージョンは dotnet new で .csproj を生成します。

データベーステーブル定義

まず、今回使用するデータベーステーブルの定義は以下のようなものを想定します。

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

テーブル名は UserAccount で、列定義は「主キーである ID、氏名を表す FirstName / LastName、TwitterアカウントIDを表す ValidatedTwitterId」という構成です。ValidatedTwitterIdとは、確かにTwitter上に対象IDが存在すると検証したTwitterIDという意味です。

プロジェクトを作成

コマンドプロンプトを開き、dotnetコマンドによりプロジェクトを作成します。
ここでは、c:\Projects\dotNet\efCore11Exampleフォルダで以下のコマンドを実行しました。

dotnet new

.csprojに参照設定を追加

本サンプルではEntity Framework 1.1 / HttpClient を利用します。
csprojファイルは以下のように PackegeReference を追加しています。

// efCore11Example.csproj
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
  
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp1.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="**\*.cs" />
    <EmbeddedResource Include="**\*.resx" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NETCore.App">
      <Version>1.0.1</Version>
    </PackageReference>
    <PackageReference Include="Microsoft.NET.Sdk">
      <Version>1.0.0-alpha-20161104-2</Version>
      <PrivateAssets>All</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.EntityFrameworkCore">
      <Version>1.1.0</Version>
    </PackageReference>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer">
      <Version>1.1.0</Version>
    </PackageReference>
    <PackageReference Include="System.Net.Http">
      <Version>4.3.0</Version>
    </PackageReference>
  </ItemGroup>
  
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

.csprojを修正した後、「dotnet restore」を実行しておきましょう。

モデルクラスの実装

UserAccountモデルクラスを実装します。

// UserAccount.cs
using System.Net.Http;
using System.Threading.Tasks;

namespace efCore11Example.Models 
{
  public class UserAccount
  {
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    private string _validatedTwitterId;

    public async Task SetTwitterId(string twitterID)
    {
      using (var client = new HttpClient())
      {
        string url = "https://twitter.com/" + twitterID;
        var response = client.GetAsync(url).Result;
        var stringResponse = await response.Content.ReadAsStringAsync();
        if( !stringResponse.Contains("<form class=\"search-404\" action=\"https://twitter.com/search\" method=\"get\">") )
        {
          this._validatedTwitterId = twitterID;
        }
      }
      
    }
    public string GetTwitterId()
    {
      return this._validatedTwitterId;
    }
  }
}

UserAccountテーブルのカラムに該当する「Id / FirstName / LastName」は、通常通りのプロパティとして実装します。
ValidatedTwitterId列に対応するプロパティは定義せず、代わりに「validatedTwitterId」フィールドを用意します。
また、
validatedTwitterIdフィールド値を設定するための「SetTwitterId()メソッド」を用意します。このメソッドは、twitterサイトにHTTPリクエストを行い、TwitterIDの存在チェックを行っています。あまり良い実装ではありませんが、Twitterに対して存在しないTiwtterIDのページをリクエストした際、
”レスポンスされたHTMLに「class="search-404"」というタグが含まれる”
という事柄を利用してTwitterIDの存在を確認しています。

DbContextクラスの実装

次に、DbContextクラスを継承したExampleDbContextクラスを実装します。

// ExampleDbContext.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

namespace efCore11Example.Models 
{
  public class ExampleDbContext : DbContext
  {
    public DbSet<UserAccount> UserAccounts { get; set; }

    public ExampleDbContext()
    {
    }
    
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
      optionsBuilder.UseSqlServer(@"Server=tcp:rdexampledb2svr.database.windows.net,1433;Initial Catalog=RdExampleDb2;Persist Security Info=False;User ID=[user id];Password=[password];MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;");
    }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
      modelBuilder.Entity<UserAccount>()
        .Property<string>("ValidatedTwitterId")
        .HasField("_validatedTwitterId")
        .UsePropertyAccessMode(PropertyAccessMode.Field);
    }
  }
}

DbSetとしてUserAccountsプロパティを定義します。
OnConfiguring()メソッドで接続先SQL Serverへの接続文字列を設定しました。ここでは私のAzure上のSQL Databaseへの接続文字列の設定を行っています。

そして、OnModelCreating()での実装が今回の肝になります。
UserAccountモデルクラスとデータベーステーブルのマッピングについて追加の情報を設定しています。
「.Property("ValidatedTwitterId")」は、ValidatedTwitterIdという名称のプロパティをモデルクラスに追加することを意味しています。つまり、マップ先のデータベーステーブルのカラム名になります。
「.HasField("validatedTwitterId").UsePropertyAccessMode(PropertyAccessMode.Field);」は、ValidatedTwitterIdプロパティに該当するモデルクラスの要素は「validatedTwitterId」であり、それは「フィールドとしてアクセス可能である」と定義しています。

以上で、モデルクラスおよびDbContextの準備が整いました。

ExampleDbContext / UserAccountを利用してDB操作を行う

main()メソッドに、ExampleDbContextを利用してUserAccountの追加 および 読み取り を行う実装を行うこととします。

// Program.cs
using System;

using efCore11Example.Models;

namespace efCore11Example
{
  class Program
  {
    static void Main(string[] args)
    {
       // レコードを追加
       AddAccountUsers();
       // レコードを読み取り
       ReadAccountUsers();
    }

    // 2件のUserAccountをDBに追加します。
    // 1件は不正なTwitterIDです。
    public static void AddAccountUsers()
    {
      using(ExampleDbContext context = new ExampleDbContext()) 
      {
        UserAccount userAccount = new UserAccount();
        userAccount.Id = 1;
        userAccount.FirstName = "ryuichi";
        userAccount.LastName = "daigo";
        userAccount.SetTwitterId("ryuichi111std").Wait();

        context.UserAccounts.Add(userAccount);

        UserAccount userAccount2 = new UserAccount();
        userAccount2.Id = 2;
        userAccount2.FirstName = "fusei";
        userAccount2.LastName = "account";
        userAccount2.SetTwitterId("fuseinatwitterid").Wait();

        context.UserAccounts.Add(userAccount2);

        context.SaveChanges();
      }
    }

    // UserAccountテーブルから読み取ってコンソール出力を行います。
    public static void ReadAccountUsers()
    {
      using(ExampleDbContext context = new ExampleDbContext()) 
      {
        foreach( var userAccount in context.UserAccounts)
        {
          System.Console.WriteLine("氏名: " + userAccount.FirstName + userAccount.LastName);
          System.Console.WriteLine("TwitterID: " + userAccount.GetTwitterId());
          System.Console.WriteLine("-----");
        }
      }
    }
  }
}

コード内コメントにもあるように2人の UserAccount を追加します。1人は存在するTwitterIdを設定し、もう1人は存在しないTwitterIdを設定しています。後者の 存在しないTiwtterId を指定したユーザーは UserAccount.SetTwitterId() 内の処理により_validatedTwitterIdフィールド には値が反映されません。

実行する

dotnet run」コマンドでプログラムを実行します。
実行結果は、以下の通りです。

C:\Projects\dotNet\efCore11Example>dotnet run
氏名: ryuichidaigo
TwitterID: ryuichi111std
-----
氏名: fuseiaccount
TwitterID:
-----

SQL Management Studioでデータベーステーブルを確認した結果は以下の画面キャプチャになります。

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

.NET Core開発で project.json / .csproj に手動で参照追加する場合・・・

こんな事ないですか?また、初心者な人、悩む事ないですか?的な記事です。

プロジェクトへの「パッケージ参照(アセンブリ参照)追加」は、Visual Studio などの統合開発環境を使用しているとGUI操作のみで完了してしまいます。
しかし、CLIVisual Studio Codeを使って開発をしている場合、project.json もしくは .csproj ファイルに手動で参照の追加を記述する必要があります。

【補足】project.json と .csproj
2016.11.23現在、.NET Coreではバージョンによってプロジェクトファイルの形式をproject.json形式としているバージョンとMSBuildの.csproj形式としているバージョンが混在しています。今後は .csproj 形式に統一(移行)すると見られます。

利用したいクラスのヘルプ及び名前空間を調べる

例えば「HttpClientクラス」を利用したい、と考えたと想定します。
(ブログ記事などをググって得られたコードスニペットからクラス名と使い方は分かったけれど、名前空間と実装アセンブリが省略されている、といったケースをここでは想定しています。)

HttpClientはその名前からも想像できますが、「HTTPリクエストの送信・HTTPレスポンスの受信」をプログラムで処理する事ができます。

まず、以下のURLに「.NET CoreのAPI reference」が公開されています。

https://docs.microsoft.com/ja-jp/dotnet/core/api/index

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

左上のテキストボックス(Filterと書かれたテキストボックス)に「HttpClient」と入力します。
続けて、フィルタリング結果の中から「HttpClient」をクリックします。

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

HttpClientが属する名前空間は以下の赤枠部分になります。
プログラム中では、これをusingしましょう。

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

同ページの一番下の部分までスクロールしてみます。
「Assembly」という項目があり、ここでは「System.Net.Http.dll」となっています。

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

つまり、HttpClientクラスは「System.Net.Http名前空間に属するクラスであり、System.Net.Http.dllアセンブリに実装されている」という事が分かりました。

nugetパッケージの調査

では次に、System.Net.Httpのnugetパッケージを調べます。
ブラウザで以下のURLにアクセスします。

https://www.nuget.org/

これは、nugetのトップページになります。
ページ上部の「Search Packages」に「System.Net.Http」と入力しEnterキーをクリックします。
検索結果が表示され、一番上の項目が今回探していたものであると分かります。

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

対象のページに移動すると、対象のパッケージの最新バージョンのページが表示され、「対象パッケージが依存するフレームワークアセンブリのバージョン」「同パッケージの過去のバージョン履歴一覧」が表示されます。
という事で、今回は2016/11/23現在の最新バージョンである 4.3.0 を利用することにします。

参照(依存)設定の記述(project.json形式の場合)

まず、project.jsonの例は以下になります。project.jsonの場合、dependencies項目への追記となります。

--- project.json ---
...省略
  "dependencies": {
    "System.Net.Http": "4.3.0",
  },
...省略

dependencies配下のキー項目が”パッケージ名”、値項目が”バージョン番号”となります。

参照(依存)設定の記述(.csproj形式の場合)

.csprojの例は以下になります。→配下にタグを追記します。

--- .csproj ---
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
  
  ...省略

  <ItemGroup>

    <PackageReference Include="System.Net.Http">
      <Version>4.3.0</Version>
    </PackageReference>
  </ItemGroup>

  ...省略
</Project>

PackageReference要素のInclude属性が”パッケージ名”、配下のVersionタグ値が”バージョン番号”となります。

バージョンを明記しない

また、参照(依存)の記述の際にバージョン番号部分を「*(ワイルドカード)」とすると、その時点での最新バージョンを参照する事を意味します。
project.json / .csprojの例は以下になります。

--- project.json ---
...省略
  "dependencies": {
    "System.Net.Http": "*",
  },
...省略
--- .csproj ---
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
  
  ...省略

  <ItemGroup>

    <PackageReference Include="System.Net.Http">
      <Version>*</Version>
    </PackageReference>
  </ItemGroup>

  ...省略
</Project>

プログラムを書く

project.json / .csprojを整えた上で dotnet restore する事で、以下の様な HttpClient を利用したプログラムのビルドが通る様になります。

using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        HttpTest().Wait();;

        Console.WriteLine("Hello World!");
    }

    static async Task HttpTest()
    {
        using( HttpClient httpClient = new HttpClient() )
        {
            var response = await httpClient.GetAsync("http://www.microsoft.com/");
            var stringResponse = await response.Content.ReadAsStringAsync();

            Console.WriteLine(stringResponse);
        }
    }
}

おまけ - Entity Framework Core のAPI Referenceはこちらです

Entity Framework Core のAPI Referenceは以下になります。

Entity Framework Core API Reference | Microsoft Docs

会社のブログができたので・・そっちにもたまに投稿します

私の所属する会社の「開発者ブログ」なるものが立ち上がりました。
ということで、そっちにもたまに投稿します!

dev.knowlbo.co.jp

という宣伝投稿でした。

本ブログは .NET Core を軸に、会社のブログへの投稿は Xamarin などのモバイル系および業務的に得たTips的なものを投稿していこうかと思っています。

ひとまず、本日は”「Visual Studio for Mac」でXamarin Forms + ASP.NET Coreシステムを作る”という投稿を行ってみました。

dev.knowlbo.co.jp

ではよろしければ覗いてくださいー

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

「Connect(); 2016」 day1を見た

「Connect(); 2016」を見終えまして、感想を。

去年に引き続き「Connect(); 2016」はDeveloperにとって非常に面白い、興味深い発表やデモが行われたのではないかと思います。
その中で、個人的に印象に残った事柄を以下にポロポロと書き記します。

Visual Studio For Mac

個人的な興味としては「Visual Studio Form Mac」に注目していました。
イベント開催前の、11/16日昼間に Visual Studio For Mac の発表について知ったのですが、一瞬「Visual Studio」の名称からWindows版と同等の物がMacに?と思い・・・いや.NET FrameworkがフルセットはMonoでしか動作しないよな・・・という事は.NET Coreか・・・そして.Net Standard 2.0での FullSet.NET & Core & Xamarin のライブラリ統合か・・・と頭をめぐりました。
そしてConnectでの発表・・「Xamarin Studio + .NET Core + Azure Support」的な=「Visual Studio For Mac」といった印象です。
最近MacではVisual Studio Codeを愛用しておりましたが、ゆくゆくはVisual Studio For Macに移行かな?的な期待も感じました。と同時にVisual Studio Codeは、これはこれで気に入っているのです。高機能であるがファットになり過ぎたWindows Visual Studioに比べてスッキリ・テキパキ、そしてアドイン周りもOSSをより感じられるところが良いのです。

MicrosoftMicrosoftプロダクトを使わない魅力

完全に狙ってだと思いますが、初めの1時間くらい(?)、それ以上(?)、の間、テクノロジーの紹介にMicrosoftプロダクトが一切登場しない、Mac / Linux / GitHub / Docker・・・デモに使用するブラウザもChrome。逆に狙いすぎてる感はありますが、以前の ASP.NET WebForms に代表されるMicrosoft独自オレオレ文化が消え去った事には非常に好感を持てました。そして、この思想にも繋がる、「Any Developer / Any App / Any Platform」がフェアーな思想なのだと思います。
良いものは良いし、悪いものは改善するし、楽しいものを楽しもう!と思います。

Visual Studio 2017 RC

Visual Studio 2017 がRCになりましたね!
まず、「来年出るんだぁ!」。そして分かりやすい良い名前だ!、と。
そう、従来のコードネーム「Visual Studio 15」は、MS開発マニア以外の多くの人から誤解を受けやすいコードネームだったと感じていたのです。VS 2015 と混同して、VS15が次期VSだと認識していないと見受けられるディベロッパーを目にしてきたので・・・。
私はもう眠いので、インストールはまた明日・・・

Linux Foundation / .NET Foundation

MicrosoftLinux Foundation に参加。そして、Google .NET Foundation に参加、とのこと。これは非常に良いことですね!
もう「俺はMS技術者だ」とか「私はJavaの人間だ」とか「Windowsは分かるけど、Linuxは分からない」とか、そんなのは時代遅れなのです!

.NET Core 1.1 / ASP.NET Core 1.1 / Entity Framework Core 1.1リリース

.NET Coreマニアとしてはうれしい情報。でも、まあ、以前からガンガンアップデートしているので Connect での発表で特別うれしいという事ではないけど、まあ心躍る発表でした。
ちなみにインストールしたのだけれど・・・

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

dotnet --info」しても 1.0.0-preview2-1-003155 と表示されているのはなぜ?3177になってない;;明日調べる・・・

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

Visual Studio と Docker の蜜月

Dockerとの親和性強化は良いですね。
ただ厳密に、DockerとWindows Server 2016のコンテナとかの技術的相違を把握しきれていなので勉強せねば・・・です。

結果・・「Connect(); 2016」jは毎年Developerにとって非常に心躍る発表や演出を含めた楽しいイベントですね!

★★Visual Studio for Mac★★

www.visualstudio.com

★★Visual Studio 2017 RC★★

www.visualstudio.com

ORMベンチマーク(RawDataAccessBencher)をAzure IaaSで実行してみた

はじめに(雑談的な・・・)

新規のシステム開発を行う際に、最近 何気に悩むのが「データアクセスのアーキテクチャ選択」だと思います。
昔々20年前くらいであれば「SQLをADO経由でゴリゴリ実行するぞ!」でOKだったのが、今では「素のADO.NET」から「Entity Frameworkのような重量級(?)のORM(Object Relational Mapper)」「軽さを追い求めて作られたMicro ORMであるDapper」などなど・・・多岐に渡ります。

.NET開発におけるORMとしてはマイクロソフト公式技術として Entity Framework がありますが、ver.1.0 リリース時からスピードの遅さが指摘されていて、なんだか「これを採用だ!」と言いにくい雰囲気が作られているようにも思います。
で、このEntity Frameworkも気が付けば フルセット.NET用は Ver.6、.NET Core用は Ver1.0 RTM となっています。

SQL隠ぺい型ORM」か「SQL明示定義型ORM」か

ORMの種類を大きく分けると、「Entity FrameworkのようなSQLを意識しない方針のORM」と「DapperやMyBatisのようなSQLを強く意識する方針のORM」があります。
私は データベースエンジニア ではなく ソフトウェアエンジニア の人間なので、前者の世界の方が個人的には好みであり、理想の世界だと思っています。
ソフトウェアを開発するにあたり、アプリケーションロジックはC#Rubyのようなオブジェクト指向言語で記述し、「データベースアクセスにはSQLという、リレーショナルデータを操作するための手続き型言語を利用する」というのはソフトウェアアーキテクチャとしては煩雑なのです。SQLが書きたいわけではなく、欲しいデータを欲しいフォーマットで取得したいだけなのだから、そしてその型はドメイン分析の結果得られたクラスオブジェクト型であってほしいのです。
SQL世界を実現するのが、Entity Frameworkであり、LINQという言語ですよね。

とはいえ、高いパフォーマンス、高負荷状態でも高速なデータ取得をリレーショナルデータベースに求めると、やはりチューニングされたSQLを実行する事が最も高速に動作するというのも事実。そして、結構こういうケースが多いと思います。
なので、「データアクセスのアーキテクチャ選択」が難しいのだと思います。
選択肢としては1つの技術を選択するだけでなく、生産性重視のORM+速度重視の別技術、など、1システム内でも機能毎に利用する技術を別々にするのも選択肢だと思います。

ベンチマークしてみた

で、改めて各種データアクセス技術のベンチマークを取ってみよう、と思いました。
利用したのは「RawDataAccessBencher」。GitHub上で公開されている 素のADO.NET および 各種ORM でのクエリーパフォーマンスを測定するベンチマークプログラムです。
Githubページ上でも、ベンチマーク結果が公開されているので、自分で動かさなくてもベンチマーク結果を参照することはできますが、ここでは改めてAzure IaaS上でベンチマークを取ってみました。

Azure IaaSは「Standard A6 (4 コア、28 GB メモリ) / Windows Server 2016 / SQL Server 2016 RTM」の環境を利用しました。
Azure IaaSを新規に作っているので、まっさらな ベンチマークには最適な環境だと思います。

データベースの準備

RawDataAccessBencherは、SQL Serverサンプルデータベースである「AdventureWorks」を利用してベンチマーク測定を行います。
AdventureWorksデータベースは、以前はSQL Serverインストーラに標準で付属していましたが、2012あたり(?)から付属しなくなりました。
以下のURLからダウンロードする事が出来ます。

Microsoft SQL Server Product Samples: Database - Downloads

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

SQL Management Studioを起動し、Databaseノードでマウス右ボタンメニュー「Restore Database...」を選択します。

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

SourceからDeviceを選択し、ダウンロードした「AdventureWorks2014.bak」ファイルを選択して、OKボタンをクリックします。

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

以上でSQL Server 2016上に AdventureWorks2014 サンプルデータベースが復元されました。

RawDataAccessBencherのビルド

Github(https://github.com/FransBouma/RawDataAccessBencher)からソリューションをCloneするかzip downloadします。
Visual Studio 2015 で RawBencher.sln をオープンします。
構成を Release に設定します。

!! 注意点 !!

2つほど注意点があります。
【1点目】
RawDataAccessBencherは、データベースへの接続文字列をApp.config(RawBencher.exe.config)に保持しますが、デフォルトでは利用データベース名は「AdventureWorks」となっています。
しかし、先程リストアしたデータベースは AdventureWorks2014 というデータベース名となっています。その為、接続データベース名の修正が必要になります。
App.config(RawBencher.exe.config)の以下の箇所のコメントを解除して有効にします。

<section name="sqlServerCatalogNameOverwrites" type="System.Configuration.NameValueSectionHandler" />

続けて、以下の箇所のコメントを解除して、更にVlue値を以下のように AdventureWorks2014 とします。

<sqlServerCatalogNameOverwrites>
  <add key="AdventureWorks" value="AdventureWorks2014" />
</sqlServerCatalogNameOverwrites>

更に以下の箇所の data source 値を適時マシン名に、また、initial catalog 値を AdventureWorks2014 に変更します。

<add name="AdventureWorks.ConnectionString.SQL Server (SqlClient)" connectionString="data source=DESKTOP-LFQS0ND;initial catalog=AdventureWorks2014;integrated security=SSPI;persist security info=False;packet size=4096" providerName="System.Data.SqlClient" />
<add name="EF.ConnectionString.SQL Server (SqlClient)" connectionString="metadata=res://*/AW.csdl|res://*/AW.ssdl|res://*/AW.msl;provider=System.Data.SqlClient;provider connection string=&quot;data source=DESKTOP-LFQS0ND;initial catalog=AdventureWorks2014;integrated security=SSPI;persist security info=False;packet size=4096&quot;" providerName="System.Data.EntityClient" />

【2点目】
「RawBencher\OriginalController.cs」ソースコード内に1箇所 AdventureWorks がハードコーディングされています。
ソースの以下の箇所を修正します。

KeysForIndividualFetches = conn.Query<int>("select top {=count} SalesOrderId from AdventureWorks.Sales.SalesOrderHeader order by SalesOrderNumber", new { count = IndividualKeysAmount }).AsList();
↓↓↓ 修正 ↓↓↓
KeysForIndividualFetches = conn.Query<int>("select top {=count} SalesOrderId from AdventureWorks2014.Sales.SalesOrderHeader order by SalesOrderNumber", new { count = IndividualKeysAmount }).AsList();

以上でビルド準備完了です。
Release構成でビルドを行い、作成された「bin\Releaseフォルダ」をAzure IaaS環境にコピーします。

ベンチマーク実行

一応、SQL Serverをクリアな状態にする為に、以下のコマンドをSQL Serverに対して実行します。

DBCC DROPCLEANBUFFERS;
DBCC FREEPROCCACHE;

コマンドプロンプトを開き、先程ビルド / IaaSにコピーした RawBencher.exe を実行します。

RawBencher.exe /a > Result.txt

「/a」パラメータを付与します。また「>」により標準出力結果を result.txt ファイルに出力するようにします。 (/a オプションを付与しないと、処理終了後にユーザー入力待ちが行われてしまいます。処理実行後、結果をファイル出力しそのまま自動的にプログラム実行は完了したい為に /a オプションを利用しています。)

結果

結果は以下の通りです。

ベンチマーク結果をダウンロード

結果からログ出力以外の主要な結果を抽出したものは以下の通り

Results per framework. Values are given as: 'mean (standard deviation)'
==============================================================================
Non-change tracking fetches, set fetches (25 runs), no caching
------------------------------------------------------------------------------
Handcoded materializer using DbDataReader                            : 319.68ms (9.93ms)    Enum: 3.93ms (0.39ms)
Handcoded materializer using DbDataReader (GetValues(array), boxing) : 351.40ms (11.97ms)   Enum: 3.82ms (0.43ms)
LINQ to DB v1.0.7.3 (v1.0.7.3) (normal)                              : 370.69ms (11.33ms)   Enum: 2.90ms (0.39ms)
Raw DbDataReader materializer using object arrays                    : 372.05ms (24.42ms)   Enum: 6.66ms (0.44ms)
LINQ to DB v1.0.7.3 (v1.0.7.3) (compiled)                            : 378.16ms (15.66ms)   Enum: 2.92ms (0.40ms)
PetaPoco Fast v4.0.3                                                 : 381.83ms (7.74ms)    Enum: 3.97ms (0.47ms)
LLBLGen Pro v5.0.0.0 (v5.0.4), Poco typed view with QuerySpec        : 430.82ms (10.68ms)   Enum: 3.12ms (0.40ms)
PetaPoco v4.0.3                                                      : 438.12ms (12.51ms)   Enum: 4.04ms (0.42ms)
LLBLGen Pro v5.0.0.0 (v5.0.4), Poco typed view with Linq             : 448.98ms (25.86ms)   Enum: 2.93ms (0.48ms)
Dapper v1.50.0.0                                                     : 470.63ms (24.97ms)   Enum: 4.10ms (1.20ms)
ServiceStack OrmLite v4.0.60.0 (v4.0.60.0)                           : 473.27ms (18.74ms)   Enum: 3.75ms (0.42ms)
Entity Framework v1.0.0.0 (v1.0.0.20622)                             : 495.84ms (17.27ms)   Enum: 3.48ms (1.12ms)
Linq to Sql v4.0.0.0 (v4.6.1586.0)                                   : 508.53ms (14.41ms)   Enum: 3.37ms (0.45ms)
Entity Framework v6.0.0.0 (v6.1.40302.0)                             : 539.37ms (19.22ms)   Enum: 3.17ms (0.36ms)
LLBLGen Pro v5.0.0.0 (v5.0.4), DataTable based TypedView             : 903.03ms (19.57ms)   Enum: 12.62ms (2.57ms)
Massive using dynamic class                                          : 1,294.43ms (21.67ms) Enum: 54.08ms (7.80ms)
Oak.DynamicDb using dynamic Dto class                                : 1,619.00ms (86.53ms) Enum: 357.62ms (81.74ms)

Change tracking fetches, set fetches (25 runs), no caching
------------------------------------------------------------------------------
DataTable, using DbDataAdapter                                       : 594.19ms (20.67ms)   Enum: 77.41ms (4.93ms)
Linq to Sql v4.0.0.0 (v4.6.1586.0)                                   : 706.63ms (30.22ms)   Enum: 3.65ms (0.40ms)
LLBLGen Pro v5.0.0.0 (v5.0.4)                                        : 787.29ms (37.16ms)   Enum: 27.01ms (1.80ms)
Entity Framework v1.0.0.0 (v1.0.0.20622)                             : 1,008.00ms (19.90ms) Enum: 3.76ms (0.34ms)
Oak.DynamicDb using typed dynamic class                              : 1,596.94ms (55.54ms) Enum: 2,268.00ms (101.37ms)
Entity Framework v6.0.0.0 (v6.1.40302.0)                             : 6,363.24ms (76.03ms) Enum: 5.31ms (0.64ms)
NHibernate v4.0.0.4000 (v4.0.4.4000)                                 : 6,473.33ms (128.28ms)    Enum: 5.47ms (1.10ms)

Non-change tracking individual fetches (100 elements, 25 runs), no caching
------------------------------------------------------------------------------
Handcoded materializer using DbDataReader                            : 0.28ms (0.02ms) per individual fetch
Handcoded materializer using DbDataReader (GetValues(array), boxing) : 0.28ms (0.02ms) per individual fetch
Dapper v1.50.0.0                                                     : 0.40ms (0.05ms) per individual fetch
ServiceStack OrmLite v4.0.60.0 (v4.0.60.0)                           : 0.41ms (0.04ms) per individual fetch
Oak.DynamicDb using dynamic Dto class                                : 0.51ms (0.06ms) per individual fetch
Raw DbDataReader materializer using object arrays                    : 0.53ms (0.06ms) per individual fetch
LINQ to DB v1.0.7.3 (v1.0.7.3) (compiled)                            : 0.78ms (0.06ms) per individual fetch
PetaPoco Fast v4.0.3                                                 : 0.82ms (0.04ms) per individual fetch
Massive using dynamic class                                          : 0.82ms (0.14ms) per individual fetch
LINQ to DB v1.0.7.3 (v1.0.7.3) (normal)                              : 0.86ms (0.03ms) per individual fetch
Entity Framework v1.0.0.0 (v1.0.0.20622)                             : 0.89ms (0.04ms) per individual fetch
LLBLGen Pro v5.0.0.0 (v5.0.4), Poco typed view with QuerySpec        : 1.02ms (0.04ms) per individual fetch
LLBLGen Pro v5.0.0.0 (v5.0.4), DataTable based TypedView             : 1.29ms (0.05ms) per individual fetch
Entity Framework v6.0.0.0 (v6.1.40302.0)                             : 1.30ms (0.04ms) per individual fetch
Linq to Sql v4.0.0.0 (v4.6.1586.0)                                   : 3.39ms (0.12ms) per individual fetch
LLBLGen Pro v5.0.0.0 (v5.0.4), Poco typed view with Linq             : 3.58ms (0.09ms) per individual fetch
PetaPoco v4.0.3                                                      : 8.72ms (0.14ms) per individual fetch

Change tracking individual fetches (100 elements, 25 runs), no caching
------------------------------------------------------------------------------
DataTable, using DbDataAdapter                                       : 0.51ms (0.09ms) per individual fetch
Oak.DynamicDb using typed dynamic class                              : 0.58ms (0.04ms) per individual fetch
LLBLGen Pro v5.0.0.0 (v5.0.4)                                        : 0.89ms (0.06ms) per individual fetch
NHibernate v4.0.0.4000 (v4.0.4.4000)                                 : 1.02ms (0.05ms) per individual fetch
Entity Framework v1.0.0.0 (v1.0.0.20622)                             : 1.03ms (0.08ms) per individual fetch
Entity Framework v6.0.0.0 (v6.1.40302.0)                             : 1.47ms (0.04ms) per individual fetch
Linq to Sql v4.0.0.0 (v4.6.1586.0)                                   : 3.64ms (0.13ms) per individual fetch

Change tracking fetches, eager load fetches, 3-node split graph, 1000 root elements (25 runs), no caching
------------------------------------------------------------------------------
Linq to Sql v4.0.0.0 (v4.6.1586.0)                                   : 177.92ms (10.03ms)
LLBLGen Pro v5.0.0.0 (v5.0.4)                                        : 227.49ms (10.30ms)
Entity Framework v1.0.0.0 (v1.0.0.20622)                             : 279.44ms (18.73ms)
NHibernate v4.0.0.4000 (v4.0.4.4000)                                 : 939.34ms (18.69ms)
Entity Framework v6.0.0.0 (v6.1.40302.0)                             : 962.27ms (18.81ms)

Async change tracking fetches, eager load fetches, 3-node split graph, 1000 root elements (25 runs), no caching
------------------------------------------------------------------------------
LLBLGen Pro v5.0.0.0 (v5.0.4)                                        : 226.60ms (8.89ms)
Entity Framework v1.0.0.0 (v1.0.0.20622)                             : 775.05ms (18.05ms)
Entity Framework v6.0.0.0 (v6.1.40302.0)                             : 1,055.69ms (25.01ms)

Change tracking fetches, set fetches (25 runs), caching
------------------------------------------------------------------------------
LLBLGen Pro v5.0.0.0 (v5.0.4)                                        : 360.75ms (139.12ms)  Enum: 27.46ms (4.68ms)

Change tracking individual fetches (100 elements, 25 runs), caching
------------------------------------------------------------------------------
LLBLGen Pro v5.0.0.0 (v5.0.4)                                        : 0.61ms (0.15ms) per individual fetch

分析とパフォーマンスの受け入れ具合は、各システムにより異なると思いますが、Entity Framework Coreが なかなかいい成績だったのではないかと思っています。
また、数年前に比べて Dapper > Entity Framework の差が縮まっているのではないかなぁ・・という印象を持ちました。
加えて重要なのは、このベンチマークはクエリーのみで更新系は測定されていません。Entity Frameworkは特に更新系パフォーマンスに懸念点が多く挙げられていたので、最新版で改めて別途ベンチマーク測定を行ってみようとは思っています。

次回のブログ更新は、Entity Framework Coreにも対応している Dapper をいじってみようかと思います。

.NET Coreでマルチプロジェクト構成のソリューションを作る

.NET Coreに限らないお話ですが・・・本ブログ 及び 各所の技術解説では単一プロジェクト構成が取られることが多いです。
これは「対象解説において”スポットを当てる技術”以外の箇所の複雑さを省く」為です。

しかし、実際のシステム開発においては、「単一プロジェクト構成」ではなく「複数プロジェクト構成」が取られることがほとんどです。
例えば、レイヤー化アーキテクチャによる分離により、「Webモジュール(DLL)」「ドメインモジュール(DLL)」「Repositoryモジュール(DLL)」のようにモジュール分割を行います。この場合の”各モジュール”が、実装ソースレベルでは”各プロジェクト”になります。

本投稿では.NET Coreにおいてマルチプロジェクト構成を実現する方法について説明します。

環境

さて、本投稿の検証環境は以下の通りです。

dotnet --info

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

OSは Mac(OSx) 、.NET Core SDKは 1.0.0-preview2-1-003155 となります。

ソリューション・プロジェクトの構成

以下のソリューション・プロジェクトの構成を前提とします。

./MultiProjSol  
    --MyLib   ※ライブラリプロジェクト  
    --Myonsole  ※コンソールアプリケーションプロジェクト(MyLib内の実装クラスを参照利用する)  

つまり「MyConsole と MyLib」の2つのプロジェクトから構成されるソリューションです。
MyConsole は MyLib内の実装を参照(利用)します。

ソリューションディレクトリの作成

本投稿では、以下のディレクトリをソリューションディレクトリとします。

/Users/daigo/projects/MultiProjSol

MyLibプロジェクトの作成

MyLibディレクトリを作成し、dotnet new -t lib コマンドでライブラリプロジェクトを作成します。

cd /Users/daigo/projects/MultiProjSol
mkdir MyLib
cd MyLib
dotnet new -t lib

上記コマンド実行後のMyLibディレクトリの内容は以下の通りです。

ls -l
total 16
-rwxr--r--  1 daigo  staff  135 10  6 13:55 Library.cs
-rwxr--r--  1 daigo  staff  244 10 14 09:48 project.json

自動生成された Library.cs を以下のように修正します。

using System;

namespace MyLib
{
    public class EchoMania
    {
        public string Accost(string message)
        {    
            return "I'm 'Echo Mania'! Your message is '" + message + "'";
        }
    }
}

MyLib名前空間にEchoManiaクラスを実装しました。
Accostメソッドは、引数で渡されたmessageをエコーバックします。

MyConsoleプロジェクトの作成

MyConsoleディレクトリを作成し、dotnet new コマンドでコンソールアプリケーションプロジェクトを作成します。

cd /Users/daigo/projects/MultiProjSol
mkdir MyConsole
cd MyConsole
dotnet new

上記コマンド実行後の MyConsoleディレクトリ の内容は以下の通りです。

ls -l
total 16
-rwxr--r--  1 daigo  staff  202 10  6 13:55 Program.cs
-rwxr--r--  1 daigo  staff  367 10 14 09:48 project.json

Program.csを以下のように修正します。

using System;
using MyLib;

namespace MyConsole
{
    public class Program
    {
        public static void Main(string[] args)
        {
            EchoMania mania = new EchoMania();
            Console.WriteLine(mania.Accost("Hello World!"));
        }
    }
}

MyLib名前空間のEchoManiaをインスタンス化し、Accost()メソッドを呼び出します。
Accost()メソッドの戻り値の値をコンソールに出力します。

また、MyLibを利用する為に「project.json」の依存関係設定「dependencies」に対して追記を行います。
修正後の project.json は以下の通りです。

{
  "version": "1.0.0-*",
  "buildOptions": {
    "debugType": "portable",
    "emitEntryPoint": true
  },
  "dependencies": {
    "MyLib": "*"
  },
  "frameworks": {
    "netcoreapp1.1": {
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.1.0-preview1-001100-00"
        }
      },
      "imports": "dnxcore50"
    }
  }
}

global.jsonを作成

「global.jsonファイルの作成」が、マルチプロジェクトを実現する為のキーポイントになります。
ソリューションのルートディレクトリ、つまり本サンプルでは「/Users/daigo/projects/MultiProjSol」に「global.json」ファイルを作成します。
global.json内の「projectsキー」にソリューションを構成する各プロジェクトパスを記述します。
以下が global.json になります。

{
    "projects":[
        "MyConsole",
        "MyLib"
    ]
}

dotnet restoreを実行

ソリューションディレクトリ(global.jsonの保存ディレクトリ)において、 dotnet restore を実行します。

dotnet restore

パッケージのリストアが行われます。

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

実行

ではスタートアッププロジェクトである「MyConsoleディレクトリ」に移動し、 dotnet run を実行します。

dotnet run

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

MyLibプロジェクトに実装した EchoManiaクラス を利用する MyConsoleアプリケーションがうまく動作しました!

本投稿では利用していませんが、Visual Studio Codeを組み合わせて利用する事で、実践的な .NET Core アプリケーションの実装は十分に可能となります。