在上一篇介紹了資料驗證的三個時機,在這一篇將會實作上一篇的內容。
首先,需要定義出一個能夠用來裝錯誤訊息的資料載體。這個Class的用處只是方便我們在3段不同地方做驗證的時候,可以儲存錯誤訊息,並且在3層互相傳遞。
再來,會定一個Wrapper,把錯誤訊息包起來,並且提供一個方法回傳,驗證是否成功。
最後,在Controller那邊的驗證(ModelStateDictionary)和Repository儲存(如果驗證失敗會丟出exception)出現錯誤訊息的時候,把這些放在Wrapper 裡面,方便統一顯示資料驗證。
首先先介紹會增加的interface和Class,然後才介紹如何實際做到Freamwork裡面。
基本上定義一個interface(IBaseError)代表一個錯誤訊息會有的欄位。基本上這個interface有兩個property,一個是儲存錯誤訊息的資訊,另外一個是儲存這個錯誤訊息對應到的Property。
因為,不是所有錯誤訊息都會有對應的欄位,因此,會用兩種實作,一個是PropertyError,代表這個錯誤訊息和Property有關聯(例如某一個欄位是必填欄位,那沒就是屬於這種類型的錯誤哦訊息)。
另外一種實作則是通用型錯誤訊息叫做GeneralError。這種錯誤是不會和某一個欄位有關的,因此只會有錯誤訊息的值,而不會有property欄位。
如果用Class Diagram表示就是:
裝在錯誤訊息的Class
在Repository層如果驗證錯誤的話,Entity Framework會丟出一個Exception。
因此,為了處理這個部份,將會定義一個自訂的Exception,可以幫忙把Entity Framework的錯誤訊息包住成為IBaseError。
Class Diagram的樣子會是:
Entity Framework驗證錯誤Exception包住的客制Exception
在Mvc裡面,ModelStateDictionary會存放錯誤訊息,並且透過HtmlHelper很方便的能夠把裡面錯誤訊息顯示出來。
但是為了避免和ModelStateDictionary綁死,因此會定義一個interface,提供需要的方法,然後在做一個ModelStateDictionary Wrapper的實作,這樣就方便Service做資料驗證。
Class Diagram會是:
資料驗證的Dictionary Class Diagram
接下來就是修改目前已有的框架,來加上剛剛上面所新增的Class。
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裡面要多一個參數,用來存放錯誤訊息的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這一層,首先需要幫忙把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來說,是很重要。