Alan Tsai 的學習筆記


學而不思則罔,思而不學則殆,不思不學則“網貸” 為現任微軟最有價值專家 (MVP)、微軟認證講師 (MCT) 、Blogger、Youtuber:記錄軟體開發的點點滴滴 著重於微軟技術、C#、ASP .NET、Azure、DevOps、Docker、AI、Chatbot、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來說,是很重要。


如果文章對您有幫助,就請我喝杯飲料吧
街口支付QR Code
街口支付QR Code
台灣 Pay QR Code
台灣 Pay QR Code
Line Pay 一卡通 QR Code
Line Pay 一卡通 QR Code
街口支付QR Code
支付寶QR Code
街口支付QR Code
微信支付QR Code
comments powered by Disqus