你的浏览器还没开启 Javascript 功能!

ASP.NET Core MVC 階層化アーキテクチャ Chap2 (Generic Repositoryで共通化を図る)

前回を通じて、ASP.NET Core MVC の階層化アーキテクチャの初歩的な触りの部分を説明できたかと思います。

また、前回の内容に対して「少し文字数が多く丁寧に読むには時間がかかる」という意見も頂きましたので、今回の内容はなるべく短く簡潔に説明するように心がけます。

今回のゴールは作成した2つのResositoryの重複部分を抜き出して共通化することです。

前提

このコンテンツで扱うこと

  • ジェネリック型の紹介
  • Generic Repositoryを用いて個別のRepositoryの共通化

開発環境

環境/ソフトウェア内容
オペレーティングシステムWindows 10 1903
.NET Core SDK3.0.100
IDEVisual Studio Code 1.39.1
BrowserGoogle Chrome 78.0.3904.70

前回のおさらい

前回はデータ操作処理を「ControllerからDbContextを直接呼出し」から「Repository経由」に変更する実装まで行いました。Resositoryのインターフェースを少し確認してみましょう。

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

namespace ds.NorthwindApp.Web.Models.Interface {
    public interface ICustomerRepository {
        Task CreateAsync (Customers customer);

        Task UpdateAsync (Customers customer);

        Task DeleteAsync (Customers customer);

        Task<Customers> GetOneAsync (string id);

        Task<IEnumerable<Customers>> GetAllAsync ();

        Task<bool> ExistsAsync (string id);
    }
}
  • ISupplierRepository
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ds.NorthwindApp.Web.Models.Interface {
    public interface ISupplierRepository {
        Task CreateAsync (Suppliers supplier);

        Task UpdateAsync (Suppliers supplier);

        Task DeleteAsync (Suppliers supplier);

        Task<Suppliers> GetOneAsync (int id);

        Task<IEnumerable<Suppliers>> GetAllAsync ();

        Task<bool> ExistsAsync (int id);
    }
}

鋭い方は既に気づいたと思いますが、2つのインターフェースの違いはメソッドの引数&戻り値のみで、他の構成はすべて同じです。

クラスが増えると似た処理を実装するのはスマートなやり方ではありません。

ジェネリック型

このようなデータ操作(CRUD)は操作対象のクラス以外、ロジックが同一のケースはジェネリック型を使うのが有効です。ジェネリック型について説明は下記のサイトに詳しく載ってあります。

IRepositoryインターフェースの作成

ジェネリックを使って、インターフェースを作成します。

  • IRepository.cs
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading.Tasks;

namespace ds.NorthwindApp.Web.Models.Interface
{
    public interface IRepository<TEntity>
        where TEntity : class
    {
        Task CreateAsync(TEntity instance);

        Task UpdateAsync(TEntity instance);

        Task DeleteAsync(TEntity instance);

        Task<TEntity> GetOneAsync(Expression<Func<TEntity, bool>> expression);

        Task<IEnumerable<TEntity>> GetAllAsync();

        Task<bool> ExistsAsync(Expression<Func<TEntity, bool>> expression);
    }
}

次に、急いでCustomerRepositoryとSupplierRepositoryを直接修正するのではなく、
インターフェースICustomerRepositoryISupplierRepositoryをIRepositoryを継承するように修正します。

  • ICustomerRepository.cs
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ds.NorthwindApp.Web.Models.Interface
{
    public interface ICustomerRepository : IRepository<Customers>
    {
        Task CreateAsync(Customers customer);

        Task UpdateAsync(Customers customer);

        Task DeleteAsync(Customers customer);

        Task<Customers> GetOneAsync(string id);

        Task<IEnumerable<Customers>> GetAllAsync();

        Task<bool> ExistsAsync(string id);
    }
}
  • ISupplierRepository.cs
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ds.NorthwindApp.Web.Models.Interface
{
    public interface ISupplierRepository : IRepository<Suppliers>
    {
        Task CreateAsync(Suppliers supplier);

        Task UpdateAsync(Suppliers supplier);

        Task DeleteAsync(Suppliers supplier);

        Task<Suppliers> GetOneAsync(int id);

        Task<IEnumerable<Suppliers>> GetAllAsync();

        Task<bool> ExistsAsync(int id);
    }
}

IRepositoryに既に同じメソッドが存在するため、Visual Studioは以下ような警告が表示されます。

ICustomerRepository、ISupplierRepositoryの継承元と重複するメソッドを削除します。

  • ICustomerRepository.cs
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ds.NorthwindApp.Web.Models.Interface
{
    public interface ICustomerRepository : IRepository<Customers>
    {
    }
}
  • ISupplierRepository.cs
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ds.NorthwindApp.Web.Models.Interface
{
    public interface ISupplierRepository : IRepository<Suppliers>
    {
    }
}

コードがかなりスッキリしましたね。
このような修正方法を行うことでCustomerRepositoryとSupplierRepositoryに対して変更を加えることが一切ないため、Controllerも修正によって影響が発生しません。

GenericRepositoryの作成

インターフェースの修正が完了したあとに、実クラスの改修を行います。
CustomerRepositoryとSupplierRepositoryのメソッドの重複部分を抜き出し、GenericRepositoryを作成します。

  • GenericRepository.cs
using ds.NorthwindApp.Web.Models.Interface;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;

namespace ds.NorthwindApp.Web.Models.Repository
{
    public class GenericRepository<TEntity> : IRepository<TEntity>
        where TEntity : class
    {

        private readonly NorthwindContext db;

        public GenericRepository(NorthwindContext context)
        {
            db = context;
        }

        public async Task CreateAsync(TEntity instance)
        {
            if (instance == null)
            {
                throw new ArgumentNullException("instance");
            }
            else
            {
                await db.Set<TEntity>().AddAsync(instance);
                await db.SaveChangesAsync();
            }
        }

        public async Task UpdateAsync(TEntity instance)
        {
            if (instance == null)
            {
                throw new ArgumentNullException("instance");
            }
            else
            {
                db.Set<TEntity>().Update(instance);
                await db.SaveChangesAsync();
            }
        }

        public async Task DeleteAsync(TEntity instance)
        {
            if (instance == null)
            {
                throw new ArgumentNullException("instance");
            }
            else
            {
                db.Set<TEntity>().Remove(instance);
                await db.SaveChangesAsync();
            }
        }

        public async Task<TEntity> GetOneAsync(Expression<Func<TEntity, bool>> expression)
        {
            return await db.Set<TEntity>().FirstOrDefaultAsync(expression);
        }

        public async Task<IEnumerable<TEntity>> GetAllAsync()
        {
            return await db.Set<TEntity>().ToListAsync();
        }

        public async Task<bool> ExistsAsync(Expression<Func<TEntity, bool>> expression)
        {
            return await db.Set<TEntity>().AnyAsync(expression);
        }

    }
}

Controllerの修正

元々ControllerはICustomerRepositoryとISupplierRepositoryを使っている部分をIRepositoryを使うように修正します。

  • CustomersController.cs
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ds.NorthwindApp.Web.Models;
using ds.NorthwindApp.Web.Models.Interface;

namespace ds.NorthwindApp.Web.Controllers
{
    public class CustomersController : Controller
    {

        private readonly IRepository<Customers> _customerRepository;

        public CustomersController(IRepository<Customers> customerRepository)
        {
            _customerRepository = customerRepository;
        }

        // GET: Customers
        public async Task<IActionResult> Index()
        {
            return View(await _customerRepository.GetAllAsync());
        }

        // GET: Customers/Details/5
        public async Task<IActionResult> Details(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var customers = await _customerRepository.GetOneAsync(x => x.CustomerId == id);
            if (customers == null)
            {
                return NotFound();
            }

            return View(customers);
        }

        // GET: Customers/Create
        public IActionResult Create()
        {
            return View();
        }

        // POST: Customers/Create
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
        // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("CustomerId,CompanyName,ContactName,ContactTitle,Address,City,Region,PostalCode,Country,Phone,Fax")] Customers customers)
        {
            if (ModelState.IsValid)
            {
                await _customerRepository.CreateAsync(customers);
                return RedirectToAction(nameof(Index));
            }
            return View(customers);
        }

        // GET: Customers/Edit/5
        public async Task<IActionResult> Edit(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var customers = await _customerRepository.GetOneAsync(x => x.CustomerId == id);
            if (customers == null)
            {
                return NotFound();
            }
            return View(customers);
        }

        // POST: Customers/Edit/5
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
        // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(string id, [Bind("CustomerId,CompanyName,ContactName,ContactTitle,Address,City,Region,PostalCode,Country,Phone,Fax")] Customers customers)
        {
            if (id != customers.CustomerId)
            {
                return NotFound();
            }

            if (ModelState.IsValid)
            {
                try
                {
                    await _customerRepository.UpdateAsync(customers);
                }
                catch (DbUpdateConcurrencyException)
                {
                    if (!await _customerRepository
                        .ExistsAsync(x => x.CustomerId == customers.CustomerId))
                    {
                        return NotFound();
                    }
                    else
                    {
                        throw;
                    }
                }
                return RedirectToAction(nameof(Index));
            }
            return View(customers);
        }

        // GET: Customers/Delete/5
        public async Task<IActionResult> Delete(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var customers = await _customerRepository.GetOneAsync(x => x.CustomerId == id);
            if (customers == null)
            {
                return NotFound();
            }

            return View(customers);
        }

        // POST: Customers/Delete/5
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(string id)
        {
            var customers = await _customerRepository.GetOneAsync(x => x.CustomerId == id);
            await _customerRepository.DeleteAsync(customers);
            return RedirectToAction(nameof(Index));
        }
    }
}
  • SuppliersController.cs
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ds.NorthwindApp.Web.Models;
using ds.NorthwindApp.Web.Models.Interface;

namespace ds.NorthwindApp.Web.Controllers
{
    public class SuppliersController : Controller
    {
        private readonly IRepository<Suppliers> _supplierRepository;

        public SuppliersController(IRepository<Suppliers> supplierRepository)
        {
            _supplierRepository = supplierRepository;
        }

        // GET: Suppliers
        public async Task<IActionResult> Index()
        {
            return View(await _supplierRepository.GetAllAsync());
        }

        // GET: Suppliers/Details/5
        public async Task<IActionResult> Details(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var suppliers = await _supplierRepository.GetOneAsync(x => x.SupplierId == id.Value);


            if (suppliers == null)
            {
                return NotFound();
            }

            return View(suppliers);
        }

        // GET: Suppliers/Create
        public IActionResult Create()
        {
            return View();
        }

        // POST: Suppliers/Create
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
        // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("SupplierId,CompanyName,ContactName,ContactTitle,Address,City,Region,PostalCode,Country,Phone,Fax,HomePage")] Suppliers suppliers)
        {
            if (ModelState.IsValid)
            {
                await _supplierRepository.CreateAsync(suppliers);
                return RedirectToAction(nameof(Index));
            }
            return View(suppliers);
        }

        // GET: Suppliers/Edit/5
        public async Task<IActionResult> Edit(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var suppliers = await _supplierRepository.GetOneAsync(x => x.SupplierId == id.Value);
            if (suppliers == null)
            {
                return NotFound();
            }
            return View(suppliers);
        }

        // POST: Suppliers/Edit/5
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
        // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(int id, [Bind("SupplierId,CompanyName,ContactName,ContactTitle,Address,City,Region,PostalCode,Country,Phone,Fax,HomePage")] Suppliers suppliers)
        {
            if (id != suppliers.SupplierId)
            {
                return NotFound();
            }

            if (ModelState.IsValid)
            {
                try
                {
                    await _supplierRepository.UpdateAsync(suppliers);
                }
                catch (DbUpdateConcurrencyException)
                {
                    if (!await _supplierRepository.ExistsAsync(x => x.SupplierId ==suppliers.SupplierId))
                    {
                        return NotFound();
                    }
                    else
                    {
                        throw;
                    }
                }
                return RedirectToAction(nameof(Index));
            }
            return View(suppliers);
        }

        // GET: Suppliers/Delete/5
        public async Task<IActionResult> Delete(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var suppliers = await _supplierRepository.GetOneAsync(x => x.SupplierId == id.Value);

            if (suppliers == null)
            {
                return NotFound();
            }

            return View(suppliers);
        }

        // POST: Suppliers/Delete/5
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(int id)
        {

            var suppliers = await _supplierRepository.GetOneAsync(x => x.SupplierId == id);
            await _supplierRepository.DeleteAsync(suppliers);
            return RedirectToAction(nameof(Index));
        }

    }
}

依存性注入

Startup.csにてIRepositoryの依存性注入を追加します。


public void ConfigureServices(IServiceCollection services)
{
    // 省略

    // Repository
    services.AddScoped(typeof(IRepository<>), typeof(GenericRepository<>));
}

個別Repositoryの削除

Controllerのデータ操作処理の置き換えが完了できましたので、個別Repositoryはもう不要です。
以下4つのソースを削除して大丈夫です。

  • ICustomerRepository.cs
  • ISupplierRepository.cs
  • CustomerRepository.cs
  • SupplierRepository.cs

まとめ

Generic(汎用) Repositoryの共通化によって、クラスが増えてもいちいち個別Repositoryを作成する必要が無くなりました。

このパターンはほとんどのデータ操作ケースを網羅できますが、
Generic(汎用) Repositoryでカバーしきれないデータ操作方法(主キーidだけでデータ取得したい ※Linq Expressionを使いたくない)があった場合、どうすれば良いでしょうか?

これに関しては次回のコンテンツにて紹介したいと思います。

備考

今回作成したソースコードです。

GitHubリポジトリ

では!!( `ー´)ノ