More

    資料驗證 – 實作篇 (.NET MVC 5 Ch-18)

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

    同步發表於我的部落格:http://alantsai2007.blogspot.com/2014/10/BuildYourOwnApplicationFrameworkOnMvc-18-DataValidation-implement.html

    基本流程

    首先,需要定義出一個能夠用來裝錯誤訊息的資料載體。這個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表示就是:

    裝在錯誤訊息的Class

    接住Repository層的驗證錯誤邏輯

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

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

    Class Diagram的樣子會是:

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

    驗證的Dictionary

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

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

    Class Diagram會是:

    資料驗證的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來說,是很重要。

    Source : https://ithelp.ithome.com.tw/articles/10159206

    Recent Articles

    spot_img

    Related Stories

    Stay on op - Ge the daily news in your inbox