WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

MVC5 での Dependency Injection

by WebSurfer 2023年4月15日 16:11

.NET Framework の ASP.NET MVC5 アプリで Microsoft.Extensions.DependencyInjection 名前空間にあるクラスを利用して Dependency Injection (DI) 機能を実装してみましたので、忘れないように備忘録として残しておきます。

MVC5 で Dependency Injection

ターゲットフレームワークが .NET Framework 4.6.1 以降であれば ASP.NET Core で DI に使われている Microsoft.Extensions.DependencyInjection 名前空間にあるクラス類を利用できるそうなので試してみた次第です。

参考にしたのはググって探して GitHub に見つかった ASP.NET MVC and ServiceCollection sample という記事です。その記事のコードをほぼそのまま Global.asax.cs にコピーすれば必要最低限の DI 機能が実装できます。

ベースとした ASP.NET MVC5 アプリのプロジェクトは Visual Studio 2022 のテンプレートを使ってターゲットフレームワークを .NET Framework 4.8 として作成したものです。

プロジェクトの作成

そのプロジェクトに NuGet から Microsoft.Extensions.DependencyInjection をインストールします。この記事を書いた時点での最新版 7.0.0 をインストールしました。(他に関係パッケージが 4 つ同時に自動的にインストールされます)

NuGet パッケージ

自動生成された Global.asax.cs を開き以下のように書き換えます。参考にした記事 ASP.NET MVC and ServiceCollection sample のコードほぼそのままです。自分なりに調べていろいろ分かったことをコメントとして追記してあります。

using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using Microsoft.Extensions.DependencyInjection;
using Mvc5DependencyInjection.Controllers;
using Mvc5DependencyInjection.Repositories;

// 下の設定により Application_Start より前に InitModule メソッド
// が実行され HTTP モジュール ServiceScopeModule が登録される
[assembly: PreApplicationStartMethod(
    typeof(Mvc5DependencyInjection.MvcApplication), "InitModule")]

namespace Mvc5DependencyInjection
{
    public class MvcApplication : System.Web.HttpApplication
    {
        public static void InitModule()
        {
            // HTTP モジュール ServiceScopeModule を登録
            RegisterModule(typeof(ServiceScopeModule));
        }

        protected void Application_Start()
        {
            // DI コンテナの作成
            var services = new ServiceCollection();

            // DI コンテナにサービスを登録する。
            // ConfigureServices メソッドは下のコード参照
            ConfigureServices(services);

            // サービスプロバイダの作成
            ServiceProvider provider = services.BuildServiceProvider();

            // HTTP モジュール ServiceScopeModule に上の provider を渡し、
            // モジュール内でサービスプロバイダを利用できるようにする
            ServiceScopeModule.SetServiceProvider(provider);

            // 下の 4 行は Global.asax.cs にもともと含まれていたもの
            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            // SetResolver メソッドは Microsoft のドキュメントによると
            // "Provides a registration point for dependency resolvers,
            // using the specified dependency resolver interface." との
            // こと。要するに、これにより MVC5 アプリで DI が実現できる
            // ようになるらしい
            var resolver = new ServiceProviderDependencyResolver();
            DependencyResolver.SetResolver(resolver);
        }

        // DI コンテナにサービスを登録するメソッド。
        // Controller については、ASP.NET Core では AddController,
        // AddMvc, AddControllersWithViews, AddRazorPages などを使うと
        // アセンブリをスキャンして全てを DI コンテナに登録するという
        // ことが行われるようだが、それはこのサンプルでは未実装。
        // すべて以下のメソッド内で登録する。
        private void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<ScopedThing>();
            services.AddTransient<HomeController>();
        }
    }

    // このサンプルではこのオブジェクトを HomeController に Inject
    public class ScopedThing : IDisposable
    {
        public ScopedThing()
        {
        }

        // Home/Index にてメッセージを取得できるように追加
        public string GetMessage()
        {
            return "Message form ScopedThing";
        }


        public void Dispose()
        {
        }
    }

    // HTTP モジュール
    // BeginRequest で IServiceScope を取得して HttpContext.Items
    // に保持する。EndRequest で IServiceScope を Dispose すること
    // により登録されたサービス (AddSingleton で登録されたものは除
    // く) は Dispose される
    internal class ServiceScopeModule : IHttpModule
    {
        private static ServiceProvider _serviceProvider;

        // インターフェイスに定義されているので実装が必要
        public void Dispose() { }

        public void Init(HttpApplication context)
        {
            context.BeginRequest += Context_BeginRequest;
            context.EndRequest += Context_EndRequest;
        }

        // IServiceScope オブジェクトを HttpContext.Items に保持
        private void Context_BeginRequest(object sender, EventArgs e)
        {
            var context = ((HttpApplication)sender).Context;
            context.Items[typeof(IServiceScope)] = 
                _serviceProvider.CreateScope();
        }

        // IServiceScope オブジェクトを Dispose する
        private void Context_EndRequest(object sender, EventArgs e)
        {
            var context = ((HttpApplication)sender).Context;
            if (context.Items[typeof(IServiceScope)] 
                is IServiceScope scope)
            {
                scope.Dispose();
            }
        }

        // Application_Start でサービスプロバイダが渡される
        public static void SetServiceProvider(
            ServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }
    }

    // BeginRequest で IServiceScope が HttpContext.Items に保持され
    // ているので、それを使って DI に登録されたサービスを取得できる
    internal class ServiceProviderDependencyResolver 
                   : IDependencyResolver
    {
        public object GetService(Type serviceType)
        {
            if (HttpContext.Current?.Items[typeof(IServiceScope)] 
                is IServiceScope scope)
            {
                return scope.ServiceProvider.GetService(serviceType);
            }

            throw new InvalidOperationException(
                                    "IServiceScope not provided");
        }

        public IEnumerable<object> GetServices(Type serviceType)
        {
            if (HttpContext.Current?.Items[typeof(IServiceScope)] 
                is IServiceScope scope)
            {
                return scope.ServiceProvider.GetServices(serviceType);
            }

            throw new InvalidOperationException(
                                      "IServiceScope not provided");
        }
    }
}

上の ConfigureServices メソッドに Controller とそれが依存するクラスを登録すれば DI 機能は働くようになります。

この記事では HomeController と ScopedThing を登録しています。自動生成された HomeController のコードに手を加えて、以下のようにコンストラクタ経由で ScopedThing を受け取れるようにすれば、クライアントからの要求を受けて HomeController が呼び出されたときに DI 機能により ScopedThing のインスタンスへの参照がコンストラクタの引数に渡されます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace Mvc5DependencyInjection.Controllers
{
    public class HomeController : Controller
    {
        private readonly ScopedThing _scopedThing;

        public HomeController(ScopedThing scopedThing)
        {
            this._scopedThing = scopedThing;
        }

        public ActionResult Index()
        {
            string msg = _scopedThing.GetMessage();
            ViewBag.Msg = msg;
            return View();
        }        
    }
}

Index アクションメソッドに書いたように、Inject された ScopedThing インスタンスの GetMessage メソッドで "Message form ScopedThing" という文字列を取得できます。

処理が終わると HttpApplication.EndRequest イベントが発生するので、HTTP モジュール ServiceScopeModule により ScopedThing の Dispose メソッドが自動的に呼ばれます。


先の記事「ASP.NET MVC5 で Autofac.Mvc5 使って DI」と同様に、下の画像のようなリポジトリパターンを実装し、「Entity データモデル」を「本番用クラス」に、「本番用クラス」を「コントローラークラス」に Inject することもできます。

リポジトリパターン

具体例を書くと、まず、先の記事と同様に (1) Visual Studio の ADO.NET Entity Data Model ウィザードで Entity Data Model (EDM) を作成し、(2) それをベースにスキャフォールディング機能を使って CRUD 用の Controller を自動生成させ、(3) Controller から SQL Server にアクセスして操作するコードをリポジトリクラスに切り出し、(4) Controller にリポジトリクラスを Inject できるようにコンストラクタを追加します。その手順の詳しい説明とコード例は先の記事にありますので見てください。

その後、作成した EDM のコンテキストクラス、リポジトリクラス、コントローラークラス を上の Global.asax.cs の ConfigureServices メソッドで DI コンテナに登録します。以下のコードで「// 追加」とコメントした下の 3 行がそれです。それだけで DI 機能が働くようになります。

private void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<ScopedThing>();
    services.AddTransient<HomeController>();

    // 追加
    services.AddScoped<NORTHWINDEntities>();
    services.AddScoped<IProductRepository, ProductRepository>();
    services.AddTransient<ProductsController>();
}

上の画像の「コントローラークラス」が ProductsController に、「インターフェイス」が IProductRepository に、「本番用クラス」が ProductRepository に、「Entity データモデル」が NORTHWINDEntities に該当します。

ブラウザから Prodtcuts/Index を要求すると、ProductsController が依存する ProductRepository、ProductRepository が依存する NORTHWINDEntities は DI 機能により自動的に Inject され、SQL Server からデータを取得して結果が表示されます。それがこの記事の一番上の画像です。

Tags: , ,

MVC

ASP.NET MVC5 で Autofac.Mvc5 使って DI

by WebSurfer 2022年10月25日 17:56

.NET Framework 4.8 の ASP.NET MVC5 アプリで Autofac.Mvc5 を利用して Dependency Injection (DI) 機能を実装してみました。忘れないように備忘録として残しておきます。

Autofac.MVC5

Visual Studio のテンプレートで作る ASP.NET MVC5 プロジェクトには DI 機能は実装されていません。Microsoft のドキュメント「ASP.NET MVC と ASP.NET Core での依存関係の挿入の相違点」にサードパーティ製の Autofac が紹介されていましたので使ってみました。

(ASP.NET Core で DI に使われている Microsoft.Extensions.DependencyInjection 名前空間にあるクラス類は .NET Framework 4.6.1 以降であれば利用できるそうなので、そちらを使うことを考えた方がいいかもしれません)

ベースとした ASP.NET MVC5 アプリは、先の記事「スキャフォールディング機能」に書いたものと同じです。Microsoft のサンプル SQL Server データベース Northwind から Entity Data Model (EDM) を作り、スキャフォールディング機能を使って Create, Read, Update, Delete (CRUD) 操作を行う Controller と View を一式自動生成しています。

スキャフォールディング機能で自動生成されたコードに手を加えてリポジトリパターンを使うように変更し、下の画像の「本番用クラス」とそれが使う EDM のコンテキストクラスを DI 機能を使って Inject できるようにしてみます。

リポジトリパターン

自動生成される Controller のコードは、内部で以下のようにコンテキストクラス NORTHWINDEntities のインスタンスを生成し、それを使って Linq to Entities で SQL Server にアクセスして操作するコードがハードコーディングされています。まず、その部分のコードを「本番用クラス」に切り出します。

using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
using System.Net;
using System.Web.Mvc;

namespace Mvc5App2.Controllers
{
    public class ProductsController : Controller
    {
        private NORTHWINDEntities db = new NORTHWINDEntities();

        // GET: Products
        public async Task<ActionResult> Index()
        {
            var products = db.Products
                           .Include(p => p.Categories)
                           .Include(p => p.Suppliers);
            return View(await products.ToListAsync());
        }

        // ・・・中略・・・

    }
}

上の図の「インターフェイス」は IProductRepository という名前で以下のようにしました。Controller には Index, Details, Create, Edit, Delete アクションメソッドがありますので、IProductRepository にはそれらが使うメソッドをすべて定義しています。非同期操作を行うので戻り値は Task<T> としています。

using System.Collections.Generic;
using System.Threading.Tasks;

namespace Mvc5AppAutofac.Models
{
    public interface IProductRepository
    {
        Task<IEnumerable<Products>> GetProducts();
        Task<Products> GetProductById(int id);
        Task<IEnumerable<Categories>> GetCatagories();
        Task<IEnumerable<Suppliers>> GetSuppliers();
        Task<int> CreateProduct(Products product);
        Task<int> UpdateProduct(Products product);
        Task<int> DeleteProduct(int id);
    }
}

上の図の「本番用クラス」は上の IProductRepository インターフェイスを継承し、ProductRepository という名前で以下のようにしました。コンテキストクラス NORTHWINDEntities は DI 機能を使ってコンストラクタ経由で Inject することを考えています。

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Threading.Tasks;

namespace Mvc5AppAutofac.Models
{
    public class ProductRepository : IProductRepository, 
                                     IDisposable
    {
        private readonly NORTHWINDEntities db;

        // Dispose パターンの実装のための変数
        private bool disposedValue;

        public ProductRepository(NORTHWINDEntities db)
        {
            this.db = db;
        }

        public async Task<IEnumerable<Products>> GetProducts()
        {
            var products = db.Products
                           .Include(p => p.Categories)
                           .Include(p => p.Suppliers);
            return await products.ToListAsync();
        }        

        public async Task<Products> GetProductById(int id)
        {
            Products product = await db.Products.FindAsync(id);
            return product;
        }

        public async Task<IEnumerable<Categories>> GetCatagories()
        {
            var categgories = db.Categories;
            return await categgories.ToListAsync();
        }

        public async Task<IEnumerable<Suppliers>> GetSuppliers()
        {
            var suppliers = db.Suppliers;
            return await suppliers.ToListAsync();
        }

        public async Task<int> CreateProduct(Products product)
        {
            db.Products.Add(product);
            return await db.SaveChangesAsync();
        }

        public async Task<int> UpdateProduct(Products product)
        {
            db.Entry(product).State = EntityState.Modified;
            return await db.SaveChangesAsync();
        }

        public async Task<int> DeleteProduct(int id)
        {
            Products products = await db.Products.FindAsync(id);
            db.Products.Remove(products);
            return await db.SaveChangesAsync();
        }

        // 以下は Dispose パターンの実装
        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    if (db != null)
                    {
                        db.Dispose();
                    }
                }

                disposedValue = true;
            }
        }

        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
    }
}

上の ProductRepository クラスは IDisposable インターフェイスも継承していますが、その理由を以下に説明します。

ProductRepository クラスは、DI 操作によりコンストラクタ経由で NORTHWINDEntities クラスのインスタンスへの参照を受け取り、それを変数 db に保持します。NORTHWINDEntities クラスは DbContext クラスを継承しており、DbContext クラスは IDisposable インターフェイスを継承していますので、使い終わったら Dispose する必要があります。

そのために、ProductRepository クラスには IDisposable インターフェイスも継承させ、Dispose パターンを実装してその中で NORTHWINDEntities オブジェクトを Dispose するようにしています。

ProductRepository クラスの Dispose メソッドは、Autofac のドキュメント Disposal の Automatic Disposal のセクションに書いてあるように、DI 機能により生成されたインスタンスの lifetime の終わりに自動的に呼び出されるそうです。デバッガを使って実際に呼び出されることは確認できました。

自動生成された Controller のコードを、DI 機能を利用してコンストラクタ経由で上の ProductRepository クラスのインスタンスへの参照を受け取れるように変更し、ProductRepository クラスに実装されたメソッドを使って SQL Server にアクセスして必要な操作ができるように書き換えます。

using System.Threading.Tasks;
using System.Net;
using System.Web.Mvc;
using Mvc5AppAutofac.Models;

namespace Mvc5AppAutofac.Controllers
{
    public class ProductsController : Controller
    {
        private readonly IProductRepository rep;

        public ProductsController(IProductRepository rep)
        {
            this.rep = rep;
        }

        // GET: Products
        public async Task<ActionResult> Index()
        {
            return View(await rep.GetProducts());
        }

        // ・・・中略・・・

    }
}

最後に、この記事の一番上の画像の Autofac.Mvc5 v6.1.0 を NuGet からインストールし、その DI 機能が働くように設定します。。

そのためには、Controller, ProductRepository, NORTHWINDEntities を DI コンテナに含めて初期化し、ASP.NET に登録する必要があります。具体的には、Global.asax にある既存の Application_Start メソッドに「Autofac.Mvc5 による DI を行うため以下のコードを追加」とコメントした下のコードを追加します。

using Autofac;
using Autofac.Integration.Mvc;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using Mvc5AppAutofac.Models;

namespace Mvc5AppAutofac
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);


            // Autofac.Mvc5 による DI を行うため以下のコードを追加

            // DI コンテナを作成するビルダのインスタンスを生成
            var builder = new ContainerBuilder();

            // アセンブリをスキャンしてすべての Controller を DI コン
            // テナに登録。下のコードの MvcApplication は Global.asax
            // のクラス名。スコープを指定しない場合はデフォルトの
            // InstancePerDependency になるらしい
            builder.RegisterControllers(typeof(MvcApplication).Assembly)
                   .InstancePerRequest();

            // ProductRepository クラスと NORTHWINDEntities クラスを
            // DI コンテナに登録。スコープを指定しない場合はデフォル
            // トの InstancePerDependency になる
            builder.RegisterType<ProductRepository>()
                   .As<IProductRepository>()
                   .InstancePerRequest();

            builder.RegisterType<NORTHWINDEntities>()
                   .InstancePerRequest();

            // DI コンテナの生成
            var container = builder.Build();

            // DI コンテナを ASP.NET に登録
            DependencyResolver.SetResolver(
                new AutofacDependencyResolver(container));
        }
    }
}

設定の説明は上のコードに付与したコメントを見てください。詳細が必要でしたら Autofac のドキュメント MVC を見てください。

ASP.NET Core に組み込みの DI 機能には DI により生成されたインスタンスの lifetime を、DI コンテナの登録する際に AddTransient, AddScoped, AddSingleton の 3 種類のメソッドを使って設定できますが、それと同様な機能は Autofac にもあります。詳しくは Autofac のドキュメント Instance Scope を見てください。

上のコード例では InstancePerRequest に設定していますが、それは ASP.NET Core 組み込みの DI 機能では AddScoped に相当します。これにより、要求ごとに DI コンテナからインスタンスが生成され、応答を返すと廃棄されます。廃棄される際、上の ProductsController クラスに実装した Dispose メソッドが呼び出されます。

以上により、ASP.NET が Controller のインスタンスを作る際 DI 機能が働いて、自動的に ProductRepository, NORTHWINDEntities クラスのインスタンスが生成され、それらへの参照がコンストラクタ経由で inject されます。

Tags: , ,

MVC

PostgreSQL で EF6 Code First

by WebSurfer 2022年7月23日 13:10

先の記事「PostgreSQL で EF6 DB First」の続きで、PostgreSQL に Entity Framework 6 を利用してコードファーストでデータベースを作成し、ASP.MET MVC5 アプリで利用する話を書きます。

PostgresSQL で EF6 Code First

ASP.NET Core + EF Core ではなく ASP.NET MVC5 + Entity Framework 6 の話ですので注意してください。

一つ注意すべきことは、エンティティクラスに TableAttribute 属性を付与してスキーマ名を設定しないとデフォルトの dbo となってしまうことです。詳しくは下のステップ (2) を見てください。

環境は以下の通りで、すべてこの記事を書いた時点での最新版です。

  • PostgreSQL 14.4
  • Visual Studio Community 2022 17.2.6
  • Npgsql PostgreSQL Integration 4.1.12
  • Entityframework6.Npgsql 6.4.3
  • Npgsql 6.0.5
  • .NET Framework 4.8
  • ASP.NET MVC 5.2.7 (VS2022 のテンプレートで作成)

手順は以下の通りです。

(1) プロジェクトの作成

Visual Studio 2022 のテンプレートで .NET Framework 4.8 の ASP.NET MVC 5.2.7 ソリューション/プロジェクトを認証なしで作成します。

(2) エンティティクラスとコンテキストクラスの追加

Microsoft のドキュメント「新しいデータベースの Code First」にあるエンティティクラスとコンテキストクラスを使ってみます。

Data フォルダを追加しその中に BloggingContext クラスを、既存の Models フォルダに Blog, Post クラスを追加します。

Blog, Post クラスには Table 属性を付与して Schema プロパティを設定することを忘れないようにしてください。PostGreSQL のデフォルトは public です。設定しないと SQL Server のデフォルト dbo になります。

BloggingContext クラスには接続文字列を指定するコンストラクタを追加します。Blog, Post, BloggingContext クラスのコードは順に以下の通りです。

Blog クラス

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace Mvc5PostgreSQL2.Models
{
    [Table("Blog", Schema = "public")]
    public class Blog
    {
        public int BlogId { get; set; }
        public string Name { get; set; }

        public virtual List<Post> Posts { get; set; }
    }
}

Post クラス

using System.ComponentModel.DataAnnotations.Schema;

namespace Mvc5PostgreSQL2.Models
{
    [Table("Post", Schema = "public")]
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        public int BlogId { get; set; }
        public virtual Blog Blog { get; set; }
    }
}

BloggingContext クラス

using Mvc5PostgreSQL2.Models;
using System.Data.Entity;

namespace Mvc5PostgreSQL2.Data
{
    public class BloggingContext : DbContext
    {
        public BloggingContext() : base("name=BloggingContext")
        {

        }

        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    }
}

(3) 接続文字列の追加

上の BloggingContext クラスのコンストラクタで "name=BloggingContext" と指定した BloggingContext という名前の接続文字列を web.config に追加します。

<connectionStrings>
  <add name="BloggingContext" 
    connectionString="Server=localhost;Port=5432;Database=Blogging;Username=postgres;Password=***"
    providerName="Npgsql" />
</connectionStrings>

(4) Enable-Migrations

Visual Studio のパッケージマネージャーコンソールから Enable-Migrations を実行します。実行すると Migrations フォルダが作られ、その中に Configuration.cs ファイルが生成されているはずです。

(5) Add-Migration

パッケージマネージャーコンソールから Add-Migration BlogInitial を実行します。BlogInitial という名前は任意です。成功すると Migrations フォルダに xxxxx_BlogInitial.cs というクラスファイルが生成されるはずです (xxxxx は作成日時)。

その内容は自分の環境では以下のようになりました。

BlogInitial.cs

このファイルを使って PostgreSQL にデータベース / テーブルが生成されます。上のコードのテーブル名、スキーマ名はステップ (2) で Blog, Post クラスに付与した Table 属性の通りとなっています。

(6) Update-Database

パッケージマネージャーコンソールから Update-Database を実行します。成功すると PostgreSQL に Blog, Post テーブルが生成されます。

 Blog, Post テーブル

(7) Controller / View の作成

ソリューションエクスプローラーで Controller フォルダを右クリックし、[追加 (D)]⇒[新規スキャフォールディングアイテム (F)...]で表示される「新規スキャフォールディングアイテムの追加」画面で[Entity Framework を使用した、ビューがある MVC5 コントローラー]を選び、以下の画像のように入力して[追加]をクリックすれば CRUD 操作のための Controller / View が一式生成されます。

スキャフォールディング

この記事の一番上の画像が、アプリを実行して Create 画面でデータを 2 件追加したものです。



【オマケの話】

上のステップ (2) で「Blog, Post クラスには Table 属性を付与して Schema プロパティを設定することを忘れないようにしてください」と書きましたが、実は、そこにハマって約 1 日悩みました。

SQL Server の場合スキーマ名はデフォルトで dbo で、EF6 もデフォルトで dbo を設定するので、Microsoft のチュートリアルなどでスキーマを設定する例は自分は見たことがないです。なので、スキーマを指定するということは全く頭の中になかったです。(汗)

また同じ失敗をしないように、忘れるとどういうことになるかを書いておきます。

Table 属性を付与しないで Add-Migration を実行すると Migrations フォルダに作成される DB 作成のベースとなるクラスファイルは以下のようになります。

BlogInitial.cs

上のコードの中でテーブル名が dbo.Blogs, dbo.Posts となっているところに注目してください。それを見て少し気にはなったのですが、とりあえず Update-Database を実行しました。

エラーなく完了したので PostgreSQL に Blog, Posts テーブルが生成された・・・はずなのですが、コマンドラインツール SQL Shell (pqsl) で探しても見つかりません。(汗)

pgAdmin 4 で探してみると、スキーマが public ではなくて dbo として作成されていました。

pgAdmin 4

上に書いたように、クラスファイルのコードの中でテーブル名 Blogs, Posts の先頭に付与されている dbo がスキーマ名と判断されたようです。

Add-Migration で生成されたファイルのテーブル名から手動で dbo. を削除してから Update-Database コマンドをかけるとスキーマは public になります。しかしそれでは Controller から DB にアクセスする際 42P01: relation "dbo.xxxxx" does not exist というエラーで失敗します。

EF6 は DB にアクセスするのに EDM が必要で、コードファーストの場合はアプリケーションの実行時にコードから生成されるそうです。想像ですが、コードから生成する際、コードにスキーマ名が指定されてないと DB に投げる SQL 文にはデフォルトの dbo が付与されるようです。

PostgreSQL 側でスキーマは public となっているのに、EF6 がスキーマ dbo を付与して SQL 文を投げるので 42P01: relation "dbo.xxxxx" does not exist というエラーで失敗したということのようです。

ちなみに、Entity Data Model ウィザードに[データベースから Code First]という既存のデータベースからコードファーストで使えるコンテキストモデルとエンティティモデルを作成できるオプションがありますが、これから生成されるエンティティクラスには [Table("public.Blog")] というように Table 属性が付与され、スキーマが指定されます。

Entity Data Model ウィザード

上に書いた手順で PostgreSQL に作成した Blog テーブル、Post テーブルから Entity Data Model ウィザードの[データベースから Code First]オプションでエンティティクラスを生成すると以下のようになります。

Blog クラス

namespace ConsoleApp4
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;

    [Table("public.Blog")]
    public partial class Blog
    {
        [System.Diagnostics.CodeAnalysis.SuppressMessage(
            "Microsoft.Usage", 
            "CA2214:DoNotCallOverridableMethodsInConstructors")]
        public Blog()
        {
            Post = new HashSet<Post>();
        }

        public int BlogId { get; set; }

        public string Name { get; set; }

        [System.Diagnostics.CodeAnalysis.SuppressMessage(
            "Microsoft.Usage", 
            "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<Post> Post { get; set; }
    }
}

Post クラス

namespace ConsoleApp4
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;

    [Table("public.Post")]
    public partial class Post
    {
        public int PostId { get; set; }

        public string Title { get; set; }

        public string Content { get; set; }

        public int BlogId { get; set; }

        public virtual Blog Blog { get; set; }
    }
}

Tags: , , , ,

MVC

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar