Alan Tsai 的學習筆記


學而不思則罔,思而不學則殆,不思不學則“網貸” 為現任微軟最有價值專家 (MVP)、微軟認證講師 (MCT) 、Blogger、Youtuber:記錄軟體開發的點點滴滴 著重於微軟技術、C#、ASP .NET、Azure、DevOps、Docker、AI、Chatbot、Data Science

[iThome 第七屆鐵人賽 16] 處理檔案上傳 2 - 放到Service層

2014-10-08 Wednesday

在上一篇,介紹了如何處理檔案上傳的部分。但是,裡面處理上傳檔案的邏輯是放在Mvc的Action裡面。

這個有一些壞處,首先,和邏輯相關的不應該寫在Controller裡面,因為Controller的工作很簡單,就只是決定Model的資料,和顯示的View是哪一個。把處理這種邏輯放在Controller裡面破會了所謂的關注點分離(SoC)的概念。

再來屬於SoC的衍生,因為如果邏輯寫在了Controller裡面,未來如果要替換邏輯或者需要做一些測試的時候,更本不好做。加上,如果別的地方也需要同樣邏輯的時候,更本沒有辦法通用。

因此,這一篇將會介紹如何把邏輯抽到Service層裡面。

功能概述

基本的邏輯在上一篇實作了在Controller裡面,現在要抽到Service裡面,尤其是通用型的Service(也就是GenericService<T>),就要以能夠動態的角度去思考如何處理這部分的邏輯,因此,定下一些規則。如果進來的ViewModel符合這些規則,就處理,要不然就不處理。

檔案處理的邏輯如果在仔細思考一下,基本上就是:

  1. HttpPostedFileBase檔案儲存 (在有檔案的情況下)
  2. 把儲存檔案的路徑放到要儲存到DB的欄位

如果要把上面的邏輯自動化的話,就變成:

  1. 找到這個ViewModel裡面是不是有HttpPostedFileBase - 並且裡面有檔案
  2. 把檔案儲存
  3. 把路勁給要存到DB的欄位即可 - 要如何知道是哪一個欄位?

關於第三點,就是這個框架處理要設定的規則,有符合規則就處理,要不然就不處理。

規則

HttpPostedFileBase這個欄位的名字是實際存到DB的欄位名字加上File

舉例來說,如果實際存到DB的欄位名字是:CoverImg,那麼對應的HttpPostedFileBase名字就是CoverImgFile

有了這個規則,就可以把檔案上傳的部分移動到Service裡面。

功能實作

接下來就看看如何實際實作功能。

在Service裡面新增ViewModel的部分

/// <summary>
/// 依照某一個ViewModel的值,產生對應的Entity並且新增到資料庫
/// </summary>
/// <typeparam name="TViewModel">ViewModel的形態</typeparam>
/// <param name="viewModel">ViewModel的Reference</param>
/// <returns>是否儲存成功</returns>
public void CreateViewModelToDatabase<TViewModel>(TViewModel viewModel)
{
    ProcessHttpPostFile(viewModel, @"D:\");

    var entity = AutoMapper.Mapper.Map<T>(viewModel);

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

    db.SaveChange();
}

上面,highlight起來就是會處理檔案上傳的部分。


這個方法基本上就是透過Reflection來找到HttpPostedFileBase,並且找到對應的檔案。如果有,就會做處理:

 /// <summary>
/// 處理ViewModel裡面形態是HttpPostedFileBase的Property。
/// 會把此property的檔案儲存到某個路徑下,並且把儲存的路勁寫在對應的Property裡面
/// </summary>
/// <param name="viewModel">要處理的ViewModel</param>
/// <param name="basePath">檔案要儲存位置的base</param>
private void ProcessHttpPostFile(object viewModel, string basePath)
{
    var properties = viewModel.GetType()
			.GetProperties(BindingFlags.Instance | 
			BindingFlags.Public | BindingFlags.FlattenHierarchy);

    foreach (var item in properties
		.Where(x => x.PropertyType == typeof(HttpPostedFileBase)))
    {
        var httpost = item.GetValue(viewModel) as HttpPostedFileBase;

        if (httpost != null 
		&& string.IsNullOrEmpty(httpost.FileName) == false)
        {
            // 如果postFile的property名稱後面一定會加File。例如:
			//CoverImgFile對應的string property名稱就是CoverImg。
            // 因此看看是否有property的名稱是postFile的名稱減去4(File是4個字)
            var fileNameProperty = properties
				.Where(x => x.Name == item.Name.Remove(item.Name.Count() - 4))
				.FirstOrDefault();

            if (fileNameProperty != null)
            {
                var savePath = Path.Combine(basePath, httpost.FileName);

                if (Directory.Exists(Path.GetDirectoryName(savePath)) == false)
                {
                    Directory.CreateDirectory(Path.GetDirectoryName(savePath));
                }

                httpost.SaveAs(savePath);

                fileNameProperty.SetValue(viewModel, httpost.FileName);
            }
        }
    }
}

Controller的變化


把處理的部分移到Service之後,在Controller裡面就變成原來的樣子:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Create post)
{
    if (ModelState.IsValid)
    {
        service.CreateViewModelToDatabase(post);
        return RedirectToAction("Index");
    }

    return View(post);
}

結語


透過Service層,可以把一些通用的邏輯抽進去,避免掉Controller有太多邏輯,造成日後邏輯無法替換和重複使用。


在接下來還會介紹更多Service還能夠包括的功能,使得這些底層的內容還需要每一次都處理。


如果文章對您有幫助,就請我喝杯飲料吧
街口支付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
2014-10-08 Wednesday
comments powered by Disqus