Alan Tsai 的學習筆記


學而不思則罔,思而不學則殆,不思不學則“網貸” 記錄軟體開發的點點滴滴 著重於微軟技術、網頁開發、DevOps:C#, Asp .net Mvc、Azure、Docker、Data Science

[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來說,是很重要。

comments powered by Disqus