2014年10月11日 星期六

[iThome 第七屆鐵人賽 18] 資料驗證 - 實作篇

在上一篇介紹了資料驗證的三個時機,在這一篇將會實作上一篇的內容。

基本流程

首先,需要定義出一個能夠用來裝錯誤訊息的資料載體。這個Class的用處只是方便我們在3段不同地方做驗證的時候,可以儲存錯誤訊息,並且在3層互相傳遞。

再來,會定一個Wrapper,把錯誤訊息包起來,並且提供一個方法回傳,驗證是否成功。

最後,在Controller那邊的驗證(ModelStateDictionary)和Repository儲存(如果驗證失敗會丟出exception)出現錯誤訊息的時候,把這些放在Wrapper 裡面,方便統一顯示資料驗證。

需要新增的Class和interface

首先先介紹會增加的interface和Class,然後才介紹如何實際做到Freamwork裡面。

定義裝載錯誤訊息的載體

基本上定義一個interface(IBaseError)代表一個錯誤訊息會有的欄位。基本上這個interface有兩個property,一個是儲存錯誤訊息的資訊,另外一個是儲存這個錯誤訊息對應到的Property。

因為,不是所有錯誤訊息都會有對應的欄位,因此,會用兩種實作,一個是PropertyError,代表這個錯誤訊息和Property有關聯(例如某一個欄位是必填欄位,那沒就是屬於這種類型的錯誤哦訊息)。

另外一種實作則是通用型錯誤訊息叫做GeneralError。這種錯誤是不會和某一個欄位有關的,因此只會有錯誤訊息的值,而不會有property欄位。

如果用Class Diagram表示就是:

image
裝在錯誤訊息的Class

接住Repository層的驗證錯誤邏輯

在Repository層如果驗證錯誤的話,Entity Framework會丟出一個Exception。

因此,爲了處理這個部份,將會定義一個自訂的Exception,可以幫忙把Entity Framework的錯誤訊息包住成為IBaseError

Class Diagram的樣子會是:

image
Entity Framework驗證錯誤Exception包住的客制Exception

驗證的Dictionary

在Mvc裡面,ModelStateDictionary會存放錯誤訊息,並且透過HtmlHelper很方便的能夠把裡面錯誤訊息顯示出來。

但是爲了避免和ModelStateDictionary綁死,因此會定義一個interface,提供需要的方法,然後在做一個ModelStateDictionary Wrapper的實作,這樣就方便Service做資料驗證。

Class Diagram會是:

image
資料驗證的Dictionary Class Diagram

框架修改的地方來使用這個驗證

接下來就是修改目前已有的框架,來加上剛剛上面所新增的Class。

Repository層的修改

Repository層需要做的事情是在存檔的時候接住驗證錯誤的Exception,並且重新包過在往上丟給Service層去接,因此:

/// <summary>
/// 實作Entity Framework Unit Of Work的class
/// </summary>
public class EFUnitOfWork : IUnitOfWork
{
/// <summary>
/// 儲存所有異動。
/// </summary>
public void Save()
{
var errors = _context.GetValidationErrors();
if (!errors.Any())
{
_context.SaveChanges();
}
else
{
throw new DatabaseValidationErrors(errors);
}
}

....
}

Service層的修改


首先是Service裡面要多一個參數,用來存放錯誤訊息的Dictionary。

/// <summary>
/// 通用行的Service layer實作
/// </summary>
/// <typeparam name="T">主要的Entity形態</typeparam>
public class GenericService<T> : IService<T>
where T : class
{
/// <summary>
/// 取得驗證資訊的字典
/// </summary>
/// <value>
/// 驗證資訊的字典
/// </value>
public IValidationDictionary ValidationDictionary { get; private set; }

/// <summary>
/// 初始化IValidationDictionary
/// </summary>
/// <param name="inValidationDictionary">要用來儲存錯誤訊息的object</param>
public void InitialiseIValidationDictionary
(IValidationDictionary inValidationDictionary)
{
ValidationDictionary = inValidationDictionary;
}
....
}

在來GenericService裡面,原本的方法也需要修改:

/// <summary>
/// 依照某一個ViewModel的值,產生對應的Entity並且新增到資料庫
/// </summary>
/// <typeparam name="TViewModel">ViewModel的形態</typeparam>
/// <param name="viewModel">ViewModel的Reference</param>
/// <returns>是否儲存成功</returns>
public bool CreateViewModelToDatabase<TViewModel>(TViewModel viewModel)
{
// 商業邏輯驗證....

if (ValidationDictionary.IsValid)
{
var entity = AutoMapper.Mapper.Map<T>(viewModel);

db.Repository<T>().Create(viewModel);

SaveChange();
}

return ValidationDictionary.IsValid;
}

/// <summary>
/// 實際儲呼叫DB儲存。如果有發生驗證錯誤,把它記錄到ValidationDictionary
/// </summary>
protected void SaveChange()
{
try
{
db.Save();
}
catch (ValidationErrors propertyErrors)
{
ValidationDictionary.AddValidationErrors(propertyErrors);
}
}

首先是以新增來說,會先做一次驗證(因為以Mvc來說,ValidationDictionary實作會是一個ModelStateDictionary的Wrapper。因此,第一層的Controller 驗證會在這裡面),如果過了,表示第一層的驗證過了。各自商業邏輯的部分就依照各自情況做調整。


在來,儲存不直接呼叫db.SaveChange(),而是透過一個方法。這個方法會把db儲存的呼叫用try catch包住,而接住的Exception則是我們在Repository層針對Repository儲存錯誤而做的處理。


Controller層的修改


最後,在Controller這一層,首先需要幫忙把ModelStateDictionary注入到Service裡面,然後驗證就直接呼叫方法並且判斷回傳的bool:

public class PostsController : Controller
{
public PostsController(IUnitOfWork inDb, IPostService inService)
{
service = inService;
service.InitialiseIValidationDictionary
(new ModelStateWrapper(this.ModelState));
db = inDb;
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Create post)
{
if (service.CreateViewModelToDatabase(post))
{
return RedirectToAction<HomeController>(x => x.Index())
.WithSuccess("修改成功");
}

return View(post);
}

...
}

雖然ModelStateDictionary也希望透過DI來注入,但是會造成死循環,因為Controller在等ModelStateDictionary,而ModelStateDictionary 又需要等Controller建立。

結語


希望透過這一篇,針對資料驗證的部份有得到統一的儲存錯誤訊息位置。這不僅讓前端顯示這些錯誤訊息的時候方便,同時3個層面的錯誤訊息都可以整合,這個對於整個Application來說,是很重要。


沒有留言 :

張貼留言