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