2013年11月14日 星期四

ASP.NET MVC 4 中 Controller 與 ApiController 做讀取、新增、更新、刪除 ( CRUD )

在 ASP.NET MVC 4 架構上,WebApi ( ApiController ) 較適合做資料處理與提供的動作,而 MVC 4 Web ( Controller ) 內比較要配合 View 層資料顯示而做異動,這時候如果要把 ApiController 和 Controller 切開來,那這兩個部分的溝通就會很常使用了。如果你是用 WebApi 與純 HTML + javascript 的架構,若是一頁蒐集各資料表的某些資料,那在 View 那一段就必須要與 WebApi 溝通多次才能將資料湊齊,再加上關聯資料,那就更複雜了,另外一點,如果各個 WebApi 的 Url 寫在頁面上,可能會有資安的危險,因為 WebApi 是開放的資料,如果不是任何人都能存取,那就不能玩此架構了。

在 Api 與 View 之間在卡一個 Controller 或者是 WebApi,做資料中繼站,把資料蒐集好在一次送到 View,而 View 收到資料也只需要拆解資料後分散到各個欄位之中。最後要 submit 整包送回 Model 也是一樣在 Controller 或者是 WebApi 解析驗證後再分散到各個 Api ,這樣資安會大大的提升。

因此此篇要講解如何在 Controller 對 WebApi 做讀取、新增、更新、刪除 ( CRUD ) 的動作。

1.

在 BaseController ( 如果沒有 BaseController 就寫在該隻 Controller 內 ) 繼承 ApiController 或 Controller,再來建置幾個變數:
public class BaseApiController : ApiController 
{

    protected DefaultConnection db = new DefaultConnection(); // 資料庫連接

    protected HttpClient client;

    protected HttpResponseMessage response;

    public BaseApiController()
    {
        client = new HttpClient();
        client.BaseAddress = new Uri("Api Url");
    }
}

2.

這時候在可以在 Controller 去非同步取得 WebApi 的結果,在宣告回傳數值前要加上 async,並且回傳數值要用 Task 包起來,說明這是個數值是非同步取得的結果,於接收值端前面要加上 await 前綴參考,表示暫停執行方法,直到等候的工作完成。所以在程式中要引用參考:
using System.Threading.Tasks;

在接著非同步取得回來的 Json 字串要轉成類別形式,需要用到 JsonConvert 的方法,所以要在 NuGet 下載 Json.NET 外掛:

程式中要再引用參考:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

3.

在 Index 的 Function 中完整程式碼:
public async Task<ActionResult> Index()
{
    response = await client.GetAsync("api/RMS/CompanyApi/");
    string t_s = await response.Content.ReadAsStringAsync();
    var rms_company = JsonConvert.DeserializeObject<List<RMS_Company>>(t_s);

    return View(rms_company);
}

在 Details 的 Function 中完整程式碼:
public async Task<ActionResult> Details(Guid? id = null)
{
    response = await client.GetAsync("api/RMS/CompanyApi/" + id);
    string t_s = await response.Content.ReadAsStringAsync();
    var rms_company = JsonConvert.DeserializeObject<RMS_Company>(t_s);

    if (rms_company == null)
    {
        return HttpNotFound();
    }
    return View(rms_company);
}

在 Create 的 Function 中完整程式碼:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create(RMS_Company rms_company)
{
    if (ModelState.IsValid)
    {
        setContent(JsonConvert.SerializeObject(rms_company));
        response = await client.PostAsync("api/RMS/CompanyApi", content);

        // db.RMS_Company.Add(rms_company);
        // db.SaveChanges();
        return RedirectToAction("Index");
    }

    return View(rms_company);
}

在 Edit 的 Function 中完整程式碼:
public async Task<ActionResult> Edit(Guid? id = null)
{
    // RMS_Company rms_company = db.RMS_Company.Find(id);
    response = await client.GetAsync("api/RMS/CompanyApi/" + id);
    string t_s = await response.Content.ReadAsStringAsync();
    var rms_company = JsonConvert.DeserializeObject<RMS_Company>(t_s);

    if (rms_company == null)
    {
        return HttpNotFound();
    }
    return View(rms_company);
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(RMS_Company rms_company)
{
    if (ModelState.IsValid)
    {
        rms_company.Updater = Guid.NewGuid();
        rms_company.UpdateOn = DateTime.Now;

        setContent(JsonConvert.SerializeObject(rms_company));
        response = await client.PutAsync("api/RMS/CompanyApi/" + rms_company.CompanyId, content);

        // db.Entry(rms_company).State = EntityState.Modified;
        // db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(rms_company);
}

在 Delete 的 Function 中完整程式碼:
public async Task<ActionResult> Delete(Guid? id = null)
{
    // RMS_Company rms_company = db.RMS_Company.Find(id);
    response = await client.GetAsync("api/RMS/CompanyApi/" + id);
    string t_s = await response.Content.ReadAsStringAsync();
    var rms_company = JsonConvert.DeserializeObject<RMS_Company>(t_s);
    
    if (rms_company == null)
    {
        return HttpNotFound();
    }
    return View(rms_company);
}

[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> DeleteConfirmed(Guid id)
{
    // RMS_Company rms_company = db.RMS_Company.Find(id);
    response = await client.DeleteAsync("api/RMS/CompanyApi/" + id);
    // db.RMS_Company.Remove(rms_company);
    // db.SaveChanges();
    return RedirectToAction("Index");
}




2013年11月12日 星期二

Model 更動欄位時後出現「未套用自動移轉,因為這可能會造成資料遺失」

當我針對一個 Model 做欄位上的字數限制更動時


要將 CompanyCode 欄位字數最大限制 (MaxLength) 從 50 改為 40,然後重新執行網站測試,結果發現以下錯誤:


這大概是程式人員在寫 MVC 碰到會很頭大的問題吧! 因為我有做 Code First 自動轉移,所以後來去找了一下怎麼處理,有人是在主控台下 Update-Database -Script -Force 把 T-Sql 叫出來給使用者確認,但我叫出來是空的。

最後找到問題所在,就是要在 「Migration\configuration.cs」裡面 AutomaticMigrationsEnabled 下加上一個 AutomaticMigrationDataLossAllowed 為 true。看起來像這樣:
public Configuration()
{
    AutomaticMigrationsEnabled = true;
    AutomaticMigrationDataLossAllowed = true;
}

因為資料本身長度為 50,如果改為 40,則原本長度 40 以上的資料會遺失,因此轉移失敗,但加上 AutomaticMigrationDataLossAllowed 的意思就是說不管那些資料重不重要,反正林北就是要轉這是一組非常有魄力的指令。推薦給有魄力的人使用。


ASP.NET MVC 4 WebApi 的 BaseApiController 之 Repository 模式

在 ASP.NET MVC 4 WebApi 專案底下,常常在 Controller 需要寫許多很類似的程式碼,只是對象 Model 不一樣做些許的改變。

在此介紹如何在 Controller 之下建置底層,使 Controller 變得只需要宣告、繼承、實作即可與原本 Controller 能做的事情一樣:
namespace RMSApi.Controllers
{
    public class CompanyController : BaseApiController
    {
        // 這裡是空的,真的是這樣
    }
}

步驟是研究與參考 Generic Repository Pattern with Entity Framework and Web API ( Entity Framework 和 Web API 的通用 Repository 模式 ),目前唯一的限制就是你的資料庫 Key 值名稱都要一樣,我目前專案已經有了這項衝突,所以這個方法介紹給有緣人,建置過程必須依照以下方式:

1.

在 Model 內新增一個 Interface 資料夾,建立一個 interface 類別 IRepository 繼承 IDisposable,裡面宣告一些常用的方法:
using System;
using System.Data.SqlClient;
using System.Linq;
using System.Linq.Expressions;
namespace RMSApi.Models.Interface
{
    public interface IRepository: IDisposable 
    {
        /* 定義需要實作的函數 */


        IQueryable<T> All<T>(string[] includes = null) where T : class;

        T Get<T>(Expression<Func<T, bool>> expression, string[] includes = null) where T : class;

        T Find<T>(Expression<Func<T, bool>> predicate, string[] includes = null) where T : class;

        IQueryable<T> Filter<T>(Expression<Func<T, bool>> predicate, string[] includes = null) where T : class;

        IQueryable<T> Filter<T>(Expression<Func<T, bool>> filter, out int total, int index = 0, int size = 50, string[] includes = null) where T : class;

        bool Contains<T>(Expression<Func<T, bool>> predicate) where T : class;

        T Create<T>(T t) where T : class;

        int Delete<T>(T t) where T : class;

        int Delete<T>(Expression<Func<T, bool>> predicate) where T : class;

        int Update<T>(T t) where T : class;

        void SaveChanges();

        void ExecuteProcedure(String procedureCommand, params SqlParameter[] sqlParams);

    }
}

2.

接著建立每個 Model 內一定會出現的資料欄位的 Interface,視習慣建立兩個或者統整到同一個檔案中,像我這裡是 CreateOn 和 UpdateOn。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace RMSApi.Models.Interface
{
    public interface ICreateOn
    {
        DateTime CreateOn { get; set; }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace RMSApi.Models.Interface
{
    public interface IUpdateOn
    {
        DateTime UpdateOn { get; set; }
    }
}

再建立一個 IIdentifier,這就是你每個 Model 的 Key 值,需要一模一樣。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace RMSApi.Models.Interface
{
    public interface IIdentifier
    {
        Guid ID { get; set; }
    }
}


3.

在 Model 內新增一個 Repository 資料夾,建立一個類別 Repository 繼承 IRepository,裡面宣告一些常用的方法:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

using System.Data;
using System.Data.SqlClient;
using RMSApi.Models.Interface;
using RMSApi.Models;
using System.Linq.Expressions;
using System.Web.Http;

namespace RMSApi.Models.Repository
{
    public class Repository : IRepository
    {
        private DefaultConnection db;

        public Repository()
        {
            db = new DefaultConnection();

            // db.Configuration.ProxyCreationEnabled = false;

            // db.Configuration.LazyLoadingEnabled = false;
        }

        public IQueryable<T> All<T>(string[] includes = null) where T : class
        {
            if (includes != null && includes.Count() > 0)
            {
                var query = db.Set<T>().Include(includes.First());
                foreach (var include in includes.Skip(1))
                    query = query.Include(include);
                return query.AsQueryable();
            }

            return db.Set<T>().AsQueryable();
        }

        public T Get<T>(Expression<Func<T, bool>> expression, string[] includes = null) where T : class
        {
            return All<T>(includes).FirstOrDefault(expression);
        }

        public virtual T Find<T>(Expression<Func<T, bool>> predicate, string[] includes = null) where T : class
        {
            if (includes != null && includes.Count() > 0)
            {
                var query = db.Set<T>().Include(includes.First());
                foreach (var include in includes.Skip(1))
                    query = query.Include(include);
                return query.FirstOrDefault<T>(predicate);
            }

            return db.Set<T>().FirstOrDefault<T>(predicate);
        }

        public virtual IQueryable<T> Filter<T>(Expression<Func<T, bool>> predicate, string[] includes = null) where T : class
        {
            if (includes != null && includes.Count() > 0)
            {
                var query = db.Set<T>().Include(includes.First());
                foreach (var include in includes.Skip(1))
                    query = query.Include(include);
                return query.Where<T>(predicate).AsQueryable<T>();
            }

            return db.Set<T>().Where<T>(predicate).AsQueryable<T>();
        }

        public virtual IQueryable<T> Filter<T>(Expression<Func<T, bool>> predicate, out int total, int index = 0, int size = 50, string[] includes = null) where T : class
        {
            int skipCount = index * size;
            IQueryable<T> _resetSet;

            if (includes != null && includes.Count() > 0)
            {
                var query = db.Set<T>().Include(includes.First());
                foreach (var include in includes.Skip(1))
                    query = query.Include(include);
                _resetSet = predicate != null ? query.Where<T>(predicate).AsQueryable() : query.AsQueryable();
            }
            else
            {
                _resetSet = predicate != null ? db.Set<T>().Where<T>(predicate).AsQueryable() : db.Set<T>().AsQueryable();
            }

            _resetSet = skipCount == 0 ? _resetSet.Take(size) : _resetSet.Skip(skipCount).Take(size);
            total = _resetSet.Count();
            return _resetSet.AsQueryable();
        }

        public virtual T Create<T>(T TObject) where T : class
        {
            if (TObject is ICreateOn)
            {
                (TObject as ICreateOn).CreateOn = DateTime.Now;
            }

            if (TObject is IUpdateOn)
            {
                (TObject as IUpdateOn).UpdateOn = DateTime.Now;
            }

            var newEntry = db.Set<T>().Add(TObject);
            db.SaveChanges();
            return newEntry;
        }

        public virtual int Delete<T>(T TObject) where T : class
        {
            db.Set<T>().Remove(TObject);
            return db.SaveChanges();
        }

        public virtual int Update<T>(T TObject) where T : class
        {
            if (TObject is IUpdateOn)
            {
                (TObject as IUpdateOn).UpdateOn = DateTime.UtcNow;
            }

            var entry = db.Entry(TObject);
            db.Set<T>().Attach(TObject);
            entry.State = EntityState.Modified;
            return db.SaveChanges();
        }

        public virtual int Delete<T>(Expression<Func<T, bool>> predicate) where T : class
        {
            var objects = Filter<T>(predicate);
            foreach (var obj in objects)
                db.Set<T>().Remove(obj);
            return db.SaveChanges();
        }

        public bool Contains<T>(Expression<Func<T, bool>> predicate) where T : class
        {
            return db.Set<T>().Count<T>(predicate) > 0;
        }

        public virtual void ExecuteProcedure(String procedureCommand, params SqlParameter[] sqlParams)
        {
            db.Database.ExecuteSqlCommand(procedureCommand, sqlParams);

        }

        public virtual void SaveChanges()
        {
            db.SaveChanges();
        }

        public void Dispose()
        {
            if (db != null)
                db.Dispose();
        }
    }
}


4.

最後建立一個類別繼承 ApiController 實作 IIdentifier,且實作在 Api 中會使用的方法 ( CRUD )。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;

using RMSApi.Models;
using RMSApi.Filters;
using RMSApi.Models.Repository;
using RMSApi.Models.Interface;
using System.Data;

namespace RMSApi.Controllers
{
    public class BaseApiController<T> : ApiController where T : class, IIdentifier
    {

        protected DefaultConnection db = new DefaultConnection();

        protected IRepository DataStore { get; set; }
        protected string[] Includes { get; set; }

        public BaseApiController()
        {
            this.DataStore = new Repository();
        }

        public virtual IEnumerable<T> Get()
        {
            return DataStore.All<T>(Includes);
        }

        public virtual T Get(Guid id)
        {
            return DataStore.Find<T>(t => t.ID == id, Includes);
        }

        public virtual void Post([FromBody]T value)
        {
            try
            {
                DataStore.Update<T>(value);
            }
            catch (OptimisticConcurrencyException ex)
            {
                throw ex;
            }
        }

        public virtual void Put([FromBody]T value)
        {
            DataStore.Create<T>(value);
        }

        public virtual void Delete(Guid id)
        {
            DataStore.Delete<T>(t => t.ID == id);
        }

        public virtual void Delete([FromBody]T value)
        {
            Delete(value.ID);
        }

        protected IEnumerable GetModelErrors()
        {
            return this.ModelState.SelectMany(x => x.Value.Errors.Select(error => error.ErrorMessage));
        }
    }
}

5.

最後在每一個 Controller 中都繼承 BaseApiController 並且帶入類別即可。

namespace RMSApi.Controllers
{
    public class CompanyController : BaseApiController
    {
        // 這裡是空的,真的是這樣
    }
}

這就是完整的把 Controller 的共同部分,也就是資料處理的部分抽離出來,讓你在 Controller 看到的程式碼更簡潔 ( 其實完全沒有了! )。如果你就在這 Controller 有獨特取得的資料方式,那就是在各隻 Controller 加上就好,比較直觀亦較好維護。




2013年11月11日 星期一

GC.SuppressFinalize 的用法

GC.SuppressFinalize ,通常使用在自己實作的 Dispose 使用之後,但我不太懂為什麼還要再使用 GC.SuppressFinalize ?

Dispose(true);
GC.SuppressFinalize(this);

後來我在網路上找到一段很有趣的解釋:

dispose告诉这个实体:哥不要你了,你可以去死了。
GC.SuppressFinalize(true); 这就是告诉系统,看到死尸了,让他去清理一下

其實光是 GC.SuppressFinalize(true) 就已經是個錯誤,正確用法是 GC.SuppressFinalize(this)

在 MSDN 上的解釋為「 要求系統不要為指定物件呼叫完成項 」,備註為「

這個方法會在物件標頭中設定位元,當系統呼叫完成項時會檢查這個位元。 obj 參數必須是這個方法的呼叫端。 如果 obj 沒有完成項,呼叫 SuppressFinalize 方法沒有作用。

實作 IDisposable 介面的物件會從 IDisposable.Dispose 方法呼叫這個方法,以免記憶體回收行程在不需要 Object.Finalize 的物件上呼叫它。



最後附上正確解釋,以下引用來自 [C#]Effective C# 條款十八:實現標準Dispose模式 的解釋:

具有解構子的物件其在被垃圾收集器回收處理時,會先被放入解構佇列之中,再交由另一個專門處理解構動作的執行緒去做解構的動作,當解構的動作完成,該物件又會被放回原來的佇列等待垃圾收集器的回收,因此其性能上的耗費會比沒有解構子的物件還來的多。由於IDisposable在實作上會習慣加入解構子做為保險措施,防止類別的使用者忘記叫用Dispose方法,造成資源的洩漏。故在釋放完資源後,我們應該隨即在後呼叫GC.SuppressFinalize,告知垃圾收集器該物件的解構動作跳過不處理。