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,告知垃圾收集器該物件的解構動作跳過不處理。