サービス層の検証 (VB)
コントローラー アクションから別のサービス レイヤーに検証ロジックを移動する方法について説明します。 このチュートリアルでは、コントローラー レイヤーからサービス レイヤーを分離することで、明確な関心の分離を維持する方法について、Stephen Walther が説明します。
このチュートリアルの目的は、ASP.NET MVC アプリケーションで検証を実行する 1 つの方法について説明することです。 このチュートリアルでは、コントローラーから別のサービス レイヤーに検証ロジックを移動する方法について説明します。
関心の分離
ASP.NET MVC アプリケーションをビルドするときは、コントローラー アクション内にデータベース ロジックを配置するべきではありません。 データベースとコントローラー ロジックを混在させると、アプリケーションの維持が時間の経過と同時に困難になります。 すべてのデータベース ロジックを個別のリポジトリ レイヤーに配置することをお勧めします。
たとえば、リスト 1 には ProductRepository という名前の単純なリポジトリが含まれています。 製品リポジトリには、アプリケーションのすべてのデータ アクセス コードが含まれています。 このリストには、製品リポジトリが実装する IProductRepository インターフェイスも含まれています。
リスト 1 - Models\ProductRepository.vb
Public Class ProductRepository
Implements IProductRepository
Private _entities As New ProductDBEntities()
Public Function ListProducts() As IEnumerable(Of Product) Implements IProductRepository.ListProducts
Return _entities.ProductSet.ToList()
End Function
Public Function CreateProduct(ByVal productToCreate As Product) As Boolean Implements IProductRepository.CreateProduct
Try
_entities.AddToProductSet(productToCreate)
_entities.SaveChanges()
Return True
Catch
Return False
End Try
End Function
End Class
Public Interface IProductRepository
Function CreateProduct(ByVal productToCreate As Product) As Boolean
Function ListProducts() As IEnumerable(Of Product)
End Interface
リスト 2 のコントローラーは、Index() アクションと Create() アクションの両方でリポジトリ レイヤーを使用します。 このコントローラーにはデータベース ロジックが含まれていないことに注意してください。 リポジトリ レイヤーを作成すると、クリーンな関心の分離を維持できます。 コントローラーはアプリケーション フロー制御ロジックを担当し、リポジトリはデータ アクセス ロジックを担当します。
リスト 2 - Controllers\ProductController.vb
Public Class ProductController
Inherits Controller
Private _repository As IProductRepository
Public Sub New()
Me.New(New ProductRepository())
End Sub
Public Sub New(ByVal repository As IProductRepository)
_repository = repository
End Sub
Public Function Index() As ActionResult
Return View(_repository.ListProducts())
End Function
'
' GET: /Product/Create
Public Function Create() As ActionResult
Return View()
End Function
'
' POST: /Product/Create
<AcceptVerbs(HttpVerbs.Post)> _
Public Function Create(<Bind(Exclude:="Id")> ByVal productToCreate As Product) As ActionResult
_repository.CreateProduct(productToCreate)
Return RedirectToAction("Index")
End Function
End Class
サービス レイヤーの作成
そのため、アプリケーション フロー制御ロジックはコントローラーに属し、データ アクセス ロジックはリポジトリに属します。 その場合、検証ロジックはどこに配置しますか? 1 つのオプションは、検証ロジックをサービス レイヤーに配置することです。
サービス レイヤーは、コントローラーとリポジトリ レイヤーの間の通信を仲介する、ASP.NET MVC アプリケーションの追加レイヤーです。 サービス レイヤーにはビジネス ロジックが含まれています。 特に、検証ロジックが含まれています。
たとえば、リスト 3 の製品サービス レイヤーには CreateProduct() メソッドがあります。 CreateProduct() メソッドは ValidateProduct() メソッドを呼び出して、製品を製品リポジトリに渡す前に新しい製品を検証します。
リスト 3 - Models\ProductService.vb
Public Class ProductService
Implements IProductService
Private _modelState As ModelStateDictionary
Private _repository As IProductRepository
Public Sub New(ByVal modelState As ModelStateDictionary, ByVal repository As IProductRepository)
_modelState = modelState
_repository = repository
End Sub
Protected Function ValidateProduct(ByVal productToValidate As Product) As Boolean
If productToValidate.Name.Trim().Length = 0 Then
_modelState.AddModelError("Name", "Name is required.")
End If
If productToValidate.Description.Trim().Length = 0 Then
_modelState.AddModelError("Description", "Description is required.")
End If
If productToValidate.UnitsInStock
製品コントローラーは、リポジトリ レイヤーの代わりにサービス レイヤーを使用するように、リスト 4 で更新されました。 コントローラー レイヤーはサービス レイヤーと通信します。 サービス レイヤーはリポジトリ レイヤーと通信します。 各レイヤーには個別の責任があります。
リスト 4 - Controllers\ProductController.vb
Public Class ProductController
Inherits Controller
Private _service As IProductService
Public Sub New()
_service = New ProductService(Me.ModelState, New ProductRepository())
End Sub
Public Sub New(ByVal service As IProductService)
_service = service
End Sub
Public Function Index() As ActionResult
Return View(_service.ListProducts())
End Function
'
' GET: /Product/Create
Public Function Create() As ActionResult
Return View()
End Function
'
' POST: /Product/Create
<AcceptVerbs(HttpVerbs.Post)> _
Public Function Create(<Bind(Exclude := "Id")> ByVal productToCreate As Product) As ActionResult
If Not _service.CreateProduct(productToCreate) Then
Return View()
End If
Return RedirectToAction("Index")
End Function
End Class
製品サービスが製品コントローラー コンストラクターに作成されていることに注目してください。 製品サービスが作成されると、モデル状態ディクショナリがサービスに渡されます。 製品サービスは、モデルの状態を使用して検証エラー メッセージをコントローラーに渡します。
サービス レイヤーの分離
コントローラーとサービス レイヤーの分離は、ある点で失敗しました。 コントローラーとサービス レイヤーは、モデルの状態を介して通信します。 言い換えると、サービス レイヤーは、ASP.NET MVC フレームワークの特定の機能に依存します。
可能な限り、サービス レイヤーをコントローラー レイヤーから分離したいと考えています。 理論的には、ASP.NET MVC アプリケーションだけでなく、あらゆる種類のアプリケーションでサービス レイヤーを使用できる必要があります。 たとえば、将来的には、アプリケーション用の WPF フロントエンドを構築したいと考えるかもしれません。 ASP.NET MVC モデルの状態への依存関係をサービス レイヤーから削除する方法を見つける必要があります。
リスト 5 では、サービス レイヤーが更新され、モデルの状態が使用されなくなりました。 代わりに、IValidationDictionary インターフェイスを実装する任意のクラスを使用します。
リスト 5 - Models\ProductService.vb (分離)
Public Class ProductService
Implements IProductService
Private _validatonDictionary As IValidationDictionary
Private _repository As IProductRepository
Public Sub New(ByVal validationDictionary As IValidationDictionary, ByVal repository As IProductRepository)
_validatonDictionary = validationDictionary
_repository = repository
End Sub
Protected Function ValidateProduct(ByVal productToValidate As Product) As Boolean
If productToValidate.Name.Trim().Length = 0 Then
_validatonDictionary.AddError("Name", "Name is required.")
End If
If productToValidate.Description.Trim().Length = 0 Then
_validatonDictionary.AddError("Description", "Description is required.")
End If
If productToValidate.UnitsInStock
IValidationDictionary インターフェイスは、リスト 6 で定義されています。 この単純なインターフェイスには、1 つのメソッドと 1 つのプロパティがあります。
リスト 6 - Models\IValidationDictionary.cs
Public Interface IValidationDictionary
Sub AddError(ByVal key As String, ByVal errorMessage As String)
ReadOnly Property IsValid() As Boolean
End Interface
ModelStateWrapper クラスという名前のリスト 7 のクラスは、IValidationDictionary インターフェイスを実装します。 ModelStateWrapper クラスをインスタンス化するには、モデル状態ディクショナリをコンストラクターに渡します。
リスト 7 - Models\ModelStateWrapper.vb
Public Class ModelStateWrapper
Implements IValidationDictionary
Private _modelState As ModelStateDictionary
Public Sub New(ByVal modelState As ModelStateDictionary)
_modelState = modelState
End Sub
#Region "IValidationDictionary Members"
Public Sub AddError(ByVal key As String, ByVal errorMessage As String) Implements IValidationDictionary.AddError
_modelState.AddModelError(key, errorMessage)
End Sub
Public ReadOnly Property IsValid() As Boolean Implements IValidationDictionary.IsValid
Get
Return _modelState.IsValid
End Get
End Property
#End Region
End Class
最後に、リスト 8 の更新されたコントローラーは、コンストラクターでサービス レイヤーを作成するときに ModelStateWrapper を使用します。
リスト 8 - Controllers\ProductController.vb
Public Class ProductController
Inherits Controller
Private _service As IProductService
Public Sub New()
_service = New ProductService(New ModelStateWrapper(Me.ModelState), New ProductRepository())
End Sub
Public Sub New(ByVal service As IProductService)
_service = service
End Sub
Public Function Index() As ActionResult
Return View(_service.ListProducts())
End Function
'
' GET: /Product/Create
Public Function Create() As ActionResult
Return View()
End Function
'
' POST: /Product/Create
<AcceptVerbs(HttpVerbs.Post)> _
Public Function Create(<Bind(Exclude := "Id")> ByVal productToCreate As Product) As ActionResult
If Not _service.CreateProduct(productToCreate) Then
Return View()
End If
Return RedirectToAction("Index")
End Function
End Class
IValidationDictionary インターフェイスと ModelStateWrapper クラスを使用すると、コントローラー レイヤーからサービス レイヤーを完全に分離できます。 サービス レイヤーは、モデルの状態に依存しなくなりました。 IValidationDictionary インターフェイスを実装する任意のクラスをサービス レイヤーに渡すことができます。 たとえば、WPF アプリケーションでは、単純なコレクション クラスを使用して IValidationDictionary インターフェイスを実装できます。
まとめ
このチュートリアルの目的は、ASP.NET MVC アプリケーションで検証を実行するための 1 つのアプローチについて説明することでした。 このチュートリアルでは、すべての検証ロジックをコントローラーから別のサービス レイヤーに移動する方法について説明しました。 また、ModelStateWrapper クラスを作成して、コントローラー レイヤーからサービス レイヤーを分離する方法についても学習しました。