サービス層の検証 (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 クラスを作成して、コントローラー レイヤーからサービス レイヤーを分離する方法についても学習しました。