サービス層の検証 (C#)Validating with a Service Layer (C#)

Stephen Waltherby Stephen Walther

コントローラーアクションから別のサービスレイヤーに検証ロジックを移動する方法について説明します。Learn how to move your validation logic out of your controller actions and into a separate service layer. このチュートリアルでは、Stephen Walther は、コントローラーレイヤーからサービス層を分離することによって、問題を明確に分離する方法について説明します。In this tutorial, Stephen Walther explains how you can maintain a sharp separation of concerns by isolating your service layer from your controller layer.

このチュートリアルの目的は、ASP.NET MVC アプリケーションで検証を実行する1つの方法を説明することです。The goal of this tutorial is to describe one method of performing validation in an ASP.NET MVC application. このチュートリアルでは、検証ロジックをコントローラーから別のサービス層に移動する方法について説明します。In this tutorial, you learn how to move your validation logic out of your controllers and into a separate service layer.

懸念事項の分離Separating Concerns

ASP.NET MVC アプリケーションをビルドする場合は、コントローラーアクション内にデータベースロジックを配置しないでください。When you build an ASP.NET MVC application, you should not place your database logic inside your controller actions. データベースとコントローラーのロジックを混在させると、時間の経過と共にアプリケーションの保守が困難になります。Mixing your database and controller logic makes your application more difficult to maintain over time. すべてのデータベースロジックを別のリポジトリレイヤーに配置することをお勧めします。The recommendation is that you place all of your database logic in a separate repository layer.

たとえば、リスト1には、ProductRepository という名前の単純なリポジトリが含まれています。For example, Listing 1 contains a simple repository named the ProductRepository. 製品リポジトリには、アプリケーションのすべてのデータアクセスコードが含まれています。The product repository contains all of the data access code for the application. この一覧には、製品リポジトリによって実装される IProductRepository インターフェイスも含まれています。The listing also includes the IProductRepository interface that the product repository implements.

リスト 1--Models\ProductRepository.csListing 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 () アクションの両方でリポジトリレイヤーを使用します。The controller in Listing 2 uses the repository layer in both its Index() and Create() actions. このコントローラーには、データベースロジックが含まれていないことに注意してください。Notice that this controller does not contain any database logic. リポジトリレイヤーを作成することで、問題の明確な分離を維持することができます。Creating a repository layer enables you to maintain a clean separation of concerns. コントローラーはアプリケーションフロー制御ロジックを担当し、リポジトリはデータアクセスロジックを担当します。Controllers are responsible for application flow control logic and the repository is responsible for data access logic.

リスト 2-コントローラー (productコントローラー)Listing 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");
        }

    }
}

サービス層の作成Creating a Service Layer

そのため、アプリケーションフロー制御ロジックはコントローラーに属し、データアクセスロジックはリポジトリに属します。So, application flow control logic belongs in a controller and data access logic belongs in a repository. その場合は、検証ロジックをどこに配置すればよいでしょうか。In that case, where do you put your validation logic? 1つの方法として、検証ロジックをサービス層に配置する方法があります。One option is to place your validation logic in a service layer.

サービスレイヤーは、コントローラーとリポジトリレイヤー間の通信を仲介する ASP.NET MVC アプリケーションの追加レイヤーです。A service layer is an additional layer in an ASP.NET MVC application that mediates communication between a controller and repository layer. サービス層にはビジネスロジックが含まれます。The service layer contains business logic. 特に、検証ロジックが含まれています。In particular, it contains validation logic.

たとえば、リスト3の product service レイヤーには CreateProduct () メソッドがあります。For example, the product service layer in Listing 3 has a CreateProduct() method. CreateProduct () メソッドは、製品リポジトリに製品を渡す前に、ValidateProduct () メソッドを呼び出して新しい製品を検証します。The CreateProduct() method calls the ValidateProduct() method to validate a new product before passing the product to the product repository.

リスト 3-Modelproductservice.csListing 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で更新されています。The Product controller has been updated in Listing 4 to use the service layer instead of the repository layer. コントローラーレイヤーは、サービスレイヤーと通信します。The controller layer talks to the service layer. サービス層は、リポジトリレイヤーと通信します。The service layer talks to the repository layer. 各レイヤーには個別の責任があります。Each layer has a separate responsibility.

リスト 4-コントローラー (productコントローラー)Listing 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");
        }

    }
}

Product service が product controller コンストラクターに作成されていることに注意してください。Notice that the product service is created in the product controller constructor. 製品サービスが作成されると、モデル状態ディクショナリがサービスに渡されます。When the product service is created, the model state dictionary is passed to the service. 製品サービスは、モデルの状態を使用して、検証エラーメッセージをコントローラーに返します。The product service uses model state to pass validation error messages back to the controller.

サービス層の分離Decoupling the Service Layer

コントローラー層とサービス層を1つの観点で分離できませんでした。We have failed to isolate the controller and service layers in one respect. コントローラー層とサービス層は、モデルの状態を介して通信します。The controller and service layers communicate through model state. つまり、サービスレイヤーは、ASP.NET MVC フレームワークの特定の機能に依存しています。In other words, the service layer has a dependency on a particular feature of the ASP.NET MVC framework.

サービス層は、可能な限りコントローラーレイヤーから分離する必要があります。We want to isolate the service layer from our controller layer as much as possible. 理論的には、ASP.NET MVC アプリケーションだけでなく、あらゆる種類のアプリケーションでサービスレイヤーを使用できるようにする必要があります。In theory, we should be able to use the service layer with any type of application and not only an ASP.NET MVC application. たとえば、将来的には、アプリケーションの WPF フロントエンドを構築することが必要になる場合があります。For example, in the future, we might want to build a WPF front-end for our application. サービスレイヤーから ASP.NET MVC モデルの状態に対する依存関係を削除する方法が見つかります。We should find a way to remove the dependency on ASP.NET MVC model state from our service layer.

リスト5では、サービス層が更新され、モデルの状態が使用されなくなりました。In Listing 5, the service layer has been updated so that it no longer uses model state. 代わりに、IValidationDictionary インターフェイスを実装する任意のクラスを使用します。Instead, it uses any class that implements the IValidationDictionary interface.

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

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で定義されています。The IValidationDictionary interface is defined in Listing 6. この単純なインターフェイスには、1つのメソッドと1つのプロパティがあります。This simple interface has a single method and a single property.

リスト 6-modelivalidationdictionary, csListing 6 - Models\IValidationDictionary.cs

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

リスト7のクラス (ModelStateWrapper クラス) には、IValidationDictionary インターフェイスが実装されています。The class in Listing 7, named the ModelStateWrapper class, implements the IValidationDictionary interface. モデル状態ディクショナリをコンストラクターに渡すことによって、ModelStateWrapper クラスをインスタンス化できます。You can instantiate the ModelStateWrapper class by passing a model state dictionary to the constructor.

リスト 7-Models\ModelStateWrapper.csListing 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 を使用します。Finally, the updated controller in Listing 8 uses the ModelStateWrapper when creating the service layer in its constructor.

8-コントローラーの一覧表示Listing 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 クラスを使用すると、サービスレイヤーをコントローラーレイヤーから完全に分離することができます。Using the IValidationDictionary interface and the ModelStateWrapper class enables us to completely isolate our service layer from our controller layer. サービス層は、モデルの状態に依存しなくなりました。The service layer is no longer dependent on model state. IValidationDictionary インターフェイスを実装する任意のクラスをサービス層に渡すことができます。You can pass any class that implements the IValidationDictionary interface to the service layer. たとえば、WPF アプリケーションでは、単純なコレクションクラスを使用して IValidationDictionary インターフェイスを実装できます。For example, a WPF application might implement the IValidationDictionary interface with a simple collection class.

まとめSummary

このチュートリアルの目的は、ASP.NET MVC アプリケーションで検証を実行する方法の1つについて説明することでした。The goal of this tutorial was to discuss one approach to performing validation in an ASP.NET MVC application. このチュートリアルでは、すべての検証ロジックをコントローラーから別のサービス層に移動する方法について学習しました。In this tutorial, you learned how to move all of your validation logic out of your controllers and into a separate service layer. また、ModelStateWrapper クラスを作成して、コントローラーレイヤーからサービス層を分離する方法についても学習しました。You also learned how to isolate your service layer from your controller layer by creating a ModelStateWrapper class.