ASP.NET Core 3.1 + EntityFramework Coreを使ってアプリを書いた際、1モデルのデータを返す方法はチュートリアルにありました。
- チュートリアル: ASP.NET Core で Web API を作成する | Microsoft Docs
- ASP.NET Core を使って Web API を作成する | Microsoft Docs
一方、一対多のリレーションがあるデータをどうやってレスポンスすれば良いか、最初のうちは分かりませんでした。
そのため、色々と試したことをメモしておきます。
目次
- 環境
- 環境構築
- モデルまわりの作成
- マイグレーションの作成
- 初期データの投入
- EF Coreから発行したSQLをコンソールへ出力
- コントローラの作成
- Rider IDEで ASP.NET Coreを起動した時の動作を変更
- 動作確認
- ソースコード
環境
- macOS 10.13.6
- Jetbrains Rider IDE 2019.3
- .NET Core 3.1.0
- SDK 3.1.100
- ASP.NET Core 3.1.0
- EntityFramework Core 3.1.0
- SQLite
- AutoMapper 9.0.0
- AutoMapper.Extensions.Microsoft.DependencyInjection 7.0.0
環境構築
まずは、Macで ASP.NET Core プロジェクトを作るまでの環境構築です。
.NET Core SDKのインストール
.NET Core SDKをHomebrewで管理しようと調べてみたところ、caskにありました。
$ brew search dotnet-sdk ==> Casks dotnet-sdk
そのため、caskを使ってインストールしました。
$ brew cask install dotnet-sdk ... ==> Linking Binary 'dotnet' to '/usr/local/bin/dotnet'. 🍺 dotnet-sdk was successfully installed!
これで dotnet
コマンドが使えるようになりました。
$ dotnet --version 3.1.100
なお、SDKのバージョンアップする場合は、以下のコマンドで行います。
$ brew cask upgrade dotnet-sdk ... ==> Linking Binary 'dotnet' to '/usr/local/bin/dotnet'. ==> Purging files for version 3.0.101,1b9f265d-ba27-4f0c-8b4d-1fd42bd8448b:2bbd64abddeeea91149df3aa39d049ae of Cask dotnet-sdk 🍺 dotnet-sdk was successfully upgraded!
ソリューションとプロジェクトを作る
今回は、dotnetコマンドを使って作成します。
ソリューションファイルの作成
$ dotnet new sln
プロジェクトファイルの作成
今回は、 EFCoreRelationSample
というプロジェクトを、Web APIテンプレートを使って作成します。
まずは用意されているテンプレートを確認します。今回は webapi
を使えば良さそうです。
$ dotnet new --list ... Templates Short Name ------------------------------------------------------------ ASP.NET Core Web App (Model-View-Controller) mvc ... ASP.NET Core Web API webapi ...
プロジェクトを作成し、ソリューションに追加します。
# プロジェクトを作成 $ dotnet new webapi -o EFCoreRelationSample # ソリューションに追加 dotnet sln add ./EFCoreRelationSample
EntityFramework CoreまわりのNuGetパッケージを追加
まずはSDKのグローバルに、EntityFramework Core (以下、 EF Core
)まわりのコマンドを追加します。
EF Core ツールリファレンス (.NET CLI)-EF Core | Microsoft Docs
$ dotnet tool install --global dotnet-ef 次のコマンドを使用してツールを呼び出せます。dotnet-ef ツール 'dotnet-ef' (バージョン '3.1.0') が正常にインストールされました。
なお、 dotnet ef
コマンドは .NET Core SDKに同梱されていません。
Announcing Entity Framework Core 3.1 and Entity Framework 6.4 | .NET Blog
続いて、EF Core関連のパッケージを追加します。
# プロジェクトのあるディレクトリへ移動 $ cd EFCoreRelationSample/ # 今回SQLiteを使うため、SQLiteパッケージを追加 $ dotnet add package Microsoft.EntityFrameworkCore.SQLite # マイグレーションなどのために追加 $ dotnet add package Microsoft.EntityFrameworkCore.Design
なお、 Microsoft.EntityFrameworkCore.SQLite
の制限内容はこちら。
SQLite Database Provider - Limitations - EF Core | Microsoft Docs
また、Microsoft.EntityFrameworkCore.Design
を追加する目的はこちら。
c# - Purpose of package "Microsoft.EntityFrameworkCore.Design" - Stack Overflow
ここまでで、ASP.NET Core & EF Coreの環境ができました。
モデルまわりの作成
今回使用するモデルまわりを作成します。
モデルの作成
今回作成する一対多のリレーションを持つモデルとして、BlogとPostを使います。公式チュートリアルとほぼ同じ型です。
リレーションシップ-EF Core | Microsoft Docs
テンプレートにはモデルを入れるディレクトリがないため、
- Controllersと同列に
Models
というディレクトリを作成 - Modelsの中にモデルファイル
BlogPostOfFk.cs
を作成
とします。
2つのモデルのリレーションについて、今回はナビゲーションプロパティを使って定義します。
EF Coreの規則として、親側にだけ定義すれば、自動でリレーションが貼られるようです。
https://docs.microsoft.com/ja-jp/ef/core/modeling/relationships?tabs=data-annotations#single-navigation-property
ただ、後述のControllerにて親から子をたぐる処理を書く際、ナビゲーションプロパティが定義されていない状態ではコンパイルが通りませんでした。
var blog = await _context.BlogOfFks .Include(p => p.PostsOfFk) // この p.PostOfFk が参照できなくてエラー .ToListAsync(); return Ok(blog);
そのため、以下のように明示的に定義します。なお、外部キーについては定義しなくても動作するようです。
using System.Collections.Generic; namespace EFCoreRelationSample.Models { public class BlogOfFk { public int Id { get; set; } public string Url { get; set; } public List<PostOfFk> PostsOfFk { get; set; } } public class PostOfFk { public int Id { get; set; } public string Content { get; set; } public BlogOfFk BlogOfFk { get; set; } } }
DbContextの作成
モデルができたので、次はDBと接続するためのDbContextを作成します。
DbContextの適切な置き場が分からなかったため、今回は Models
の下に MyContext.cs
として作成しました。
DbContextを継承した MyContext
クラスを作成し、
- コンストラクタ
- DbSet
型のpublicプロパティ
を用意します。
using Microsoft.EntityFrameworkCore; namespace EFCoreRelationSample.Models { public class MyContext : DbContext { public MyContext(DbContextOptions<MyContext> options) : base(options) {} // モデルクラスへのアクセス (DbSet<TModel>) 型のpublicプロパティ public DbSet<BlogOfFk> BlogOfFks { get; set; } public DbSet<PostOfFk> PostOfFks { get; set; } } }
接続文字列の作成 (appsettings.json)
次に、SQLiteへの接続するための接続文字列を用意します。
今回は、プロジェクトルート直下にある appsettings.json
の中に記載します。
接続するSQLiteは、プロジェクトルート直下の WebApplication.db
とします。
{ // ... "AllowedHosts": "*", // 以下を追加 "ConnectionStrings": { "MyContext": "Data Source=./WebApplication.db" } }
なお、SQLiteファイルはEF Coreのマイグレーションにより自動生成されます。
Startup.csに、DbContextと接続文字列を追加
今回使用するDbContextと接続文字列を追加します。
SQLiteへ接続するため、 UseSqlite()
を使います。
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); // 以下を追加 services.AddDbContext<MyContext>(options => options.UseSqlite( Configuration.GetConnectionString("MyContext"))); }
マイグレーションの作成
モデルまわりができたため、EF Coreによるマイグレーションを作成します。
マイグレーションファイルの作成
以下のコマンドを実行すると、 Migrations
ディレクトリの中にマイグレーションファイルが作成されます。
$ dotnet ef migrations add InitialCreate
今回は
- MyContextModelSnapshot.cs
- <タイムスタンプ>_InitialCreate.cs
の2ファイルが作成されました。
データベースへの反映
$ dotnet ef database update
実行時のログより、BlogOfFk・PostOfFk モデルを元に、2つのテーブルを作成するSQLが発行されたことが分かります。
BlogOfFk
info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] CREATE TABLE "BlogOfFks" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_BlogOfFks" PRIMARY KEY AUTOINCREMENT, "Url" TEXT NULL );
PostOfFk
info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] CREATE TABLE "PostOfFks" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_PostOfFks" PRIMARY KEY AUTOINCREMENT, "Content" TEXT NULL, "BlogOfFkId" INTEGER NULL, CONSTRAINT "FK_PostOfFks_BlogOfFks_BlogOfFkId" FOREIGN KEY ("BlogOfFkId") REFERENCES "BlogOfFks" ("Id") ON DELETE RESTRICT );
モデルにはナビゲーションプロパティだけ定義したものの、子モデルの外部キー BlogOfFkId
が自動生成されました。
一方、モデルに定義した
- コレクションナビゲーションプロパティ (BlogOfFk)
- 参照ナビゲーションプロパティ (PostsOfFk)
は、テーブルのフィールドとして作成されませんでした。
初期データの投入
初期データを投入するため、データのシード処理を実装します。
データシード処理-EF Core | Microsoft Docs
投入方法はいくつかあるようですが、今回はDbContextの中で実装します。
OnModelCreating()
メソッドをオーバーライドして実装します。
namespace EFCoreRelationSample.Models { public class MyContext : DbContext { // ... protected override void OnModelCreating(ModelBuilder modelBuilder) { // データのシードを定義 // 親データ modelBuilder.Entity<BlogOfFk>().HasData( new {Id = 1, Url = "https://example.com/foo"}, new {Id = 2, Url = "https://example.com/bar"} ); // 子データ modelBuilder.Entity<PostOfFk>().HasData( new {Id = 1, Content = "ふー", BlogOfFkId = 1}, new {Id = 2, Content = "ばー", BlogOfFkId = 1} ); } } }
初期データを投入するため、
- 先ほど作成したマイグレーションファイル (Migrations/ の下) を削除
- SQLite (WebApplication.db) を削除
dotnet ef migrations add InitialCreate
dotnet ef database update
を実行したところ
info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] INSERT INTO "BlogOfFks" ("Id", "Url") VALUES (1, 'https://example.com/foo'); ... info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] INSERT INTO "PostOfFks" ("Id", "BlogOfFkId", "Content") VALUES (1, 1, 'ふー');
などのログが表示され、シード処理が行われていることが確認できました。
Rider IDEで確認しても、以下のようにデータが存在しています。
BlogOfFk
PostOfFk
EF Coreから発行したSQLをコンソールへ出力
コントローラを作成する前に、EF Coreで発行したSQLをコンソールで確認する設定を追加しておきます。
基本的には以下の記事の通りですが、.NET Core 3.1版では少々変更がありました。
【C#】Entity Framework Core の SQL の実行ログを確認する - Qiita
NuGetパッケージを追加
コンソールへ出力するためのNuGetパッケージを追加します。
$ dotnet add package Microsoft.Extensions.Logging.Console
DbContextへ追加
DbContextへ
- staticなreadonlyフィールドを追加
- OnConfiguring()をオーバーライドして実装
を追加します。
ただ、.NET Core 3.1ではLoggerFactoryの追加方法が変わったようです。
- .net core - How to create a LoggerFactory with a ConsoleLoggerProvider? - Stack Overflow
- c# - .NET Core 3.0 ConsoleLoggerFactory for SQLite - Stack Overflow
そのため
- LoggerFactory.Create()で生成
- AddFilter()で、
DbLoggerCategory.Database.Command.Name
を対象にする - AddConsole()で、コンソールへ出力
とします。
static readonly ILoggerFactory loggerFactory = LoggerFactory.Create(builder => { builder .AddFilter(DbLoggerCategory.Database.Command.Name, LogLevel.Information) .AddConsole(); }); // UseLoggerFactory で上記のロガーファクトリを登録する。 // Insert 文の値をロギングするために EnableSensitiveDataLogging を許可する。 protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .EnableSensitiveDataLogging() .UseLoggerFactory(loggerFactory);
コントローラの作成
あとはコントローラを作成すれば完成です。
失敗:Include()を使用したリレーションデータのレスポンス
コントローラに必要な
- DbContextフィールド
- コンストラクタで、DbContextフィールドへの設定
- HTTP GETでアクセスした時のメソッド
を実装します。
以下は、 /BlogPost/p2c
にアクセスした時に、BlogOfFkモデルからPostOfFkモデルをたぐって表示する例となります。
namespace EFCoreRelationSample.Controllers { [ApiController] [Route("api/[controller]")] public class BlogPostController : ControllerBase { private readonly MyContext _context; public BlogPostController(MyContext context) { this._context = context; } // 1 -> N の順でたぐる場合 [HttpGet("p2c", Name = "Parent2Child")] public async Task<IActionResult> P2C() { var blog = await _context.BlogOfFks .Include(p => p.PostsOfFk) .ToListAsync(); return Ok(blog); } } }
Rider IDEで実行し、ブラウザで https://localhost5000/api/BlogPost/p2c
へアクセスすると
An unhandled exception occurred while processing the request. JsonException: A possible object cycle was detected which is not supported. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 32.
というエラーが表示されました。
おそらく、モデルに
- コレクションナビゲーションプロパティ (BlogOfFk)
- 参照ナビゲーションプロパティ (PostsOfFk)
の2つが実装されているため、循環してしまったものと考えられました。
対応ですが、ASP.NET Core 2系では
- 関連データとシリアル化 | 関連データの読み込み - EF Core | Microsoft Docs
- ASP.NET Core で循環参照するオブジェクトを JSON へシリアル化した際に JsonSerializationException: Self referencing loop detected がスローされる問題への対処方 - Qiita
のように、 SerializerSettings.ReferenceLoopHandling
を設定すれば良いようでした。
ただ、ASP.NET Core 3系では、JSONライブラリに変更が入りました。
その影響で、SerializerSettings.ReferenceLoopHandling
を設定するには、NuGetパッケージ Microsoft.AspNetCore.Mvc.NewtonsoftJson
を追加する必要がありそうでした。
NewtonsoftJson
の追加も考えましたが、今回は AutoMapper
を使うことで対応することにしました。
AutoMapperを使った、リレーションデータのレスポンス
AutoMapperを使うにあたり、以下の記事が参考になりました。
ASP.NET CoreでAutoMapperを使う - ryuichi111stdの技術日記
ただ、ASP.NET Core3.1では少々変更されていました。
そのため、上記記事とstackoverflowを参考に
- NuGetパッケージの追加
- DTOクラスを追加
- Profileクラスを追加
- Startup.csへの追加
- コントローラへの追加
を行いました。
c# - How to set up Automapper in ASP.NET Core - Stack Overflow
NuGetパッケージの追加
必要なNuGetパッケージを追加します。
# AutoMapper本体を追加 dotnet add package AutoMapper # 拡張も入れる dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
DTOクラスを追加
ブラウザ側へ返す時のデータ型となるDTOクラスを追加します。
今回は DTO/BlogPostOfFKDTO.cs
として追加しました。
また、DTOのプロパティ名はモデルと同一としました。
using System.Collections.Generic; using EFCoreRelationSample.Models; namespace EFCoreRelationSample.DTO { public class PostOfFkDTO { // Postのフィールド public int Id { get; set; } public string Content { get; set; } // FKのフィールド public BlogOfFk BlogOfFk { get; set; } } public class BlogOfFkDTO { // Blogのフィールド public int Id { get; set; } public string Url { get; set; } // FK(Post)のフィールド public List<PostOfFk> PostsOfFk { get; set; } } }
Profileクラスを追加
次に、モデルとDTOを結びつけるProfileクラスを追加します。
置き場所が分からなかったため、今回は DTO/MyProfile.cs
として作成しました。
Profileクラスでは CreateMap()
を使って、モデルとDTOを結びつけます。
using AutoMapper; using EFCoreRelationSample.Models; namespace EFCoreRelationSample.DTO { public class MyProfile : Profile { public MyProfile() { CreateMap<BlogOfFk, BlogOfFkDTO>(); CreateMap<PostOfFk, PostOfFkDTO>(); } } }
Startup.csへの追加
ConfigureServices()
に、 AddAutoMapper()
を追加します。
public void ConfigureServices(IServiceCollection services) { ... // 【追加】AutoMapperを追加 services.AddAutoMapper(typeof(MyProfile)); }
コントローラへの追加
まず、
- IMapper型のフィールドを追加
- コンストラクタに
IMapper
型の引数を追加
を行います。
public class BlogPostController : ControllerBase { private readonly IMapper _mapper; public BlogPostController(MyContext context, IMapper mapper) { this._context = context; this._mapper = mapper; } ... }
続いてコントローラのメソッドを書き換えます。
ただ、今回はAutoMapper有無を比較できるよう P2CWithAutoMapper()
を新しく作成してみました。
AutoMapperを使う場合は、 ProjectTo()
を追加することで、DTOへの変換を行っています。
[HttpGet("p2c-auto", Name = "Parent2ChildWithAutoMapper")] public async Task<IActionResult> P2CWithAutoMapper() { var blogs = await _context.BlogOfFks .Include(p => p.PostsOfFk) .ProjectTo<BlogOfFkDTO>(_mapper.ConfigurationProvider) // 追加 .ToListAsync(); return Ok(blogs); }
ちなみに、同じような機能に Map()
もあります。 ProjectTo()
との違いは、こちらが参考になりました。
LINQ to Entities の遅延評価と AutoMapper が便利という話 - Qiita
Rider IDEで ASP.NET Coreを起動した時の動作を変更
あとは確認するだけです。
ただ、Rider IDEでASP.NET Coreを起動した時の動作を変更したいため、 Properties/launchSettings.json
を修正します。
launchBrowser
で、起動した時にブラウザも自動で開くかlaunchUrl
で、ブラウザで最初にアクセスするURLを指定
などができます。
"EFCoreRelationSample": { "commandName": "Project", "launchBrowser": true, "launchUrl": "api/BlogPost", "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }
動作確認
ASP.NET Coreアプリを起動し、ブラウザで https://localhost5000/api/BlogPost/p2c-auto
を開くと、リレーションデータのJSONが表示されました。
ソースコード
GitHubに上げました。
https://github.com/thinkAmi-sandbox/ASP_Net_Core-sample