サービス層の検証 (C#)

投稿者: Stephen Walther

コントローラー アクションから別のサービス レイヤーに検証ロジックを移動する方法について説明します。 このチュートリアルでは、コントローラー レイヤーからサービス レイヤーを分離することで、明確な関心の分離を維持する方法について、Stephen Walther が説明します。

このチュートリアルの目的は、ASP.NET MVC アプリケーションで検証を実行する 1 つの方法について説明することです。 このチュートリアルでは、コントローラーから別のサービス レイヤーに検証ロジックを移動する方法について説明します。

関心の分離

ASP.NET MVC アプリケーションをビルドするときは、コントローラー アクション内にデータベース ロジックを配置するべきではありません。 データベースとコントローラー ロジックを混在させると、アプリケーションの維持が時間の経過と同時に困難になります。 すべてのデータベース ロジックを個別のリポジトリ レイヤーに配置することをお勧めします。

たとえば、リスト 1 には ProductRepository という名前の単純なリポジトリが含まれています。 製品リポジトリには、アプリケーションのすべてのデータ アクセス コードが含まれています。 このリストには、製品リポジトリが実装する IProductRepository インターフェイスも含まれています。

リスト 1 - Models\ProductRepository.cs

using System.Collections.Generic;
using System.Linq;

namespace MvcApplication1.Models
{
    public class ProductRepository : MvcApplication1.Models.IProductRepository
    {
        private ProductDBEntities _entities = new ProductDBEntities();

        public IEnumerable<Product> ListProducts()
        {
            return _entities.ProductSet.ToList();
        }

        public bool CreateProduct(Product productToCreate)
        {
            try
            {
                _entities.AddToProductSet(productToCreate);
                _entities.SaveChanges();
                return true;
            }
            catch
            {
                return false;
            }
        }

    }

    public interface IProductRepository
    {
        bool CreateProduct(Product productToCreate);
        IEnumerable<Product> ListProducts();
    }

}

リスト 2 のコントローラーは、Index() アクションと Create() アクションの両方でリポジトリ レイヤーを使用します。 このコントローラーにはデータベース ロジックが含まれていないことに注意してください。 リポジトリ レイヤーを作成すると、クリーンな関心の分離を維持できます。 コントローラーはアプリケーション フロー制御ロジックを担当し、リポジトリはデータ アクセス ロジックを担当します。

リスト 2 - Controllers\ProductController.cs

using System.Web.Mvc;
using MvcApplication1.Models;

namespace MvcApplication1.Controllers
{
    public class ProductController : Controller
    {
        private IProductRepository _repository;

        public ProductController():
            this(new ProductRepository()) {}

        public ProductController(IProductRepository repository)
        {
            _repository = repository;
        }

        public ActionResult Index()
        {
            return View(_repository.ListProducts());
        }

        //
        // GET: /Product/Create

        public ActionResult Create()
        {
            return View();
        } 

        //
        // POST: /Product/Create

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create([Bind(Exclude="Id")] Product productToCreate)
        {
            _repository.CreateProduct(productToCreate);
            return RedirectToAction("Index");
        }

    }
}

サービス レイヤーの作成

そのため、アプリケーション フロー制御ロジックはコントローラーに属し、データ アクセス ロジックはリポジトリに属します。 その場合、検証ロジックはどこに配置しますか? 1 つのオプションは、検証ロジックをサービス レイヤーに配置することです。

サービス レイヤーは、コントローラーとリポジトリ レイヤーの間の通信を仲介する、ASP.NET MVC アプリケーションの追加レイヤーです。 サービス レイヤーにはビジネス ロジックが含まれています。 特に、検証ロジックが含まれています。

たとえば、リスト 3 の製品サービス レイヤーには CreateProduct() メソッドがあります。 CreateProduct() メソッドは ValidateProduct() メソッドを呼び出して、製品を製品リポジトリに渡す前に新しい製品を検証します。

リスト 3 - Models\ProductService.cs

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

namespace MvcApplication1.Models
{
    public class ProductService : IProductService
    {

        private ModelStateDictionary _modelState;
        private IProductRepository _repository;

        public ProductService(ModelStateDictionary modelState, IProductRepository repository)
        {
            _modelState = modelState;
            _repository = repository;
        }

        protected bool ValidateProduct(Product productToValidate)
        {
            if (productToValidate.Name.Trim().Length == 0)
                _modelState.AddModelError("Name", "Name is required.");
            if (productToValidate.Description.Trim().Length == 0)
                _modelState.AddModelError("Description", "Description is required.");
            if (productToValidate.UnitsInStock < 0)
                _modelState.AddModelError("UnitsInStock", "Units in stock cannot be less than zero.");
            return _modelState.IsValid;
        }

        public IEnumerable<Product> ListProducts()
        {
            return _repository.ListProducts();
        }

        public bool CreateProduct(Product productToCreate)
        {
            // Validation logic
            if (!ValidateProduct(productToCreate))
                return false;

            // Database logic
            try
            {
                _repository.CreateProduct(productToCreate);
            }
            catch
            {
                return false;
            }
            return true;
        }

    }

    public interface IProductService
    {
        bool CreateProduct(Product productToCreate);
        IEnumerable<Product> ListProducts();
    }
}

製品コントローラーは、リポジトリ レイヤーの代わりにサービス レイヤーを使用するように、リスト 4 で更新されました。 コントローラー レイヤーはサービス レイヤーと通信します。 サービス レイヤーはリポジトリ レイヤーと通信します。 各レイヤーには個別の責任があります。

リスト 4 - Controllers\ProductController.cs

Listing 4 – Controllers\ProductController.cs
using System.Web.Mvc;
using MvcApplication1.Models;

namespace MvcApplication1.Controllers
{
    public class ProductController : Controller
    {
        private IProductService _service;

        public ProductController() 
        {
            _service = new ProductService(this.ModelState, new ProductRepository());
        }

        public ProductController(IProductService service)
        {
            _service = service;
        }

        public ActionResult Index()
        {
            return View(_service.ListProducts());
        }

        //
        // GET: /Product/Create

        public ActionResult Create()
        {
            return View();
        }

        //
        // POST: /Product/Create

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create([Bind(Exclude = "Id")] Product productToCreate)
        {
            if (!_service.CreateProduct(productToCreate))
                return View();
            return RedirectToAction("Index");
        }

    }
}

製品サービスが製品コントローラー コンストラクターに作成されていることに注目してください。 製品サービスが作成されると、モデル状態ディクショナリがサービスに渡されます。 製品サービスは、モデルの状態を使用して検証エラー メッセージをコントローラーに渡します。

サービス レイヤーの分離

コントローラーとサービス レイヤーの分離は、ある点で失敗しました。 コントローラーとサービス レイヤーは、モデルの状態を介して通信します。 言い換えると、サービス レイヤーは、ASP.NET MVC フレームワークの特定の機能に依存します。

可能な限り、サービス レイヤーをコントローラー レイヤーから分離したいと考えています。 理論的には、ASP.NET MVC アプリケーションだけでなく、あらゆる種類のアプリケーションでサービス レイヤーを使用できる必要があります。 たとえば、将来的には、アプリケーション用の WPF フロントエンドを構築したいと考えるかもしれません。 ASP.NET MVC モデルの状態への依存関係をサービス レイヤーから削除する方法を見つける必要があります。

リスト 5 では、サービス レイヤーが更新され、モデルの状態が使用されなくなりました。 代わりに、IValidationDictionary インターフェイスを実装する任意のクラスを使用します。

リスト 5 - Models\ProductService.cs (分離)

using System.Collections.Generic;

namespace MvcApplication1.Models
{
    public class ProductService : IProductService
    {

        private IValidationDictionary _validatonDictionary;
        private IProductRepository _repository;

        public ProductService(IValidationDictionary validationDictionary, IProductRepository repository)
        {
            _validatonDictionary = validationDictionary;
            _repository = repository;
        }

        protected bool ValidateProduct(Product productToValidate)
        {
            if (productToValidate.Name.Trim().Length == 0)
                _validatonDictionary.AddError("Name", "Name is required.");
            if (productToValidate.Description.Trim().Length == 0)
                _validatonDictionary.AddError("Description", "Description is required.");
            if (productToValidate.UnitsInStock < 0)
                _validatonDictionary.AddError("UnitsInStock", "Units in stock cannot be less than zero.");
            return _validatonDictionary.IsValid;
        }

        public IEnumerable<Product> ListProducts()
        {
            return _repository.ListProducts();
        }

        public bool CreateProduct(Product productToCreate)
        {
            // Validation logic
            if (!ValidateProduct(productToCreate))
                return false;

            // Database logic
            try
            {
                _repository.CreateProduct(productToCreate);
            }
            catch
            {
                return false;
            }
            return true;
        }

    }

    public interface IProductService
    {
        bool CreateProduct(Product productToCreate);
        IEnumerable<Product> ListProducts();
    }
}

IValidationDictionary インターフェイスは、リスト 6 で定義されています。 この単純なインターフェイスには、1 つのメソッドと 1 つのプロパティがあります。

リスト 6 - Models\IValidationDictionary.cs

namespace MvcApplication1.Models
{
    public interface IValidationDictionary
    {
        void AddError(string key, string errorMessage);
        bool IsValid { get; }
    }
}

ModelStateWrapper クラスという名前のリスト 7 のクラスは、IValidationDictionary インターフェイスを実装します。 ModelStateWrapper クラスをインスタンス化するには、モデル状態ディクショナリをコンストラクターに渡します。

リスト 7 - Models\ModelStateWrapper.cs

using System.Web.Mvc;

namespace MvcApplication1.Models
{
    public class ModelStateWrapper : IValidationDictionary
    {

        private ModelStateDictionary _modelState;

        public ModelStateWrapper(ModelStateDictionary modelState)
        {
            _modelState = modelState;
        }

        #region IValidationDictionary Members

        public void AddError(string key, string errorMessage)
        {
            _modelState.AddModelError(key, errorMessage);
        }

        public bool IsValid
        {
            get { return _modelState.IsValid; }
        }

        #endregion
    }
}

最後に、リスト 8 の更新されたコントローラーは、コンストラクターでサービス レイヤーを作成するときに ModelStateWrapper を使用します。

リスト 8 - Controllers\ProductController.cs

using System.Web.Mvc;
using MvcApplication1.Models;

namespace MvcApplication1.Controllers
{
    public class ProductController : Controller
    {
        private IProductService _service;

        public ProductController() 
        {
            _service = new ProductService(new ModelStateWrapper(this.ModelState), new ProductRepository());
        }

        public ProductController(IProductService service)
        {
            _service = service;
        }

        public ActionResult Index()
        {
            return View(_service.ListProducts());
        }

        //
        // GET: /Product/Create

        public ActionResult Create()
        {
            return View();
        }

        //
        // POST: /Product/Create

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create([Bind(Exclude = "Id")] Product productToCreate)
        {
            if (!_service.CreateProduct(productToCreate))
                return View();
            return RedirectToAction("Index");
        }

    }
}

IValidationDictionary インターフェイスと ModelStateWrapper クラスを使用すると、コントローラー レイヤーからサービス レイヤーを完全に分離できます。 サービス レイヤーは、モデルの状態に依存しなくなりました。 IValidationDictionary インターフェイスを実装する任意のクラスをサービス レイヤーに渡すことができます。 たとえば、WPF アプリケーションでは、単純なコレクション クラスを使用して IValidationDictionary インターフェイスを実装できます。

まとめ

このチュートリアルの目的は、ASP.NET MVC アプリケーションで検証を実行するための 1 つのアプローチについて説明することでした。 このチュートリアルでは、すべての検証ロジックをコントローラーから別のサービス レイヤーに移動する方法について説明しました。 また、ModelStateWrapper クラスを作成して、コントローラー レイヤーからサービス レイヤーを分離する方法についても学習しました。