2014年8月1日 星期五

管理 ASPNET MVC 中的 Entity Framework DbContext 的生命週期

在應用程式中管理 DbContext 實例是非常重要的, 一個 DbContext 使用了資料庫連線(database connections)這樣重要的資源且需要被釋放(released), 如果沒有正確的 dispose 一個 DbContext 實例, 那麼相關的資料庫連線可能不會被釋放回連線池(connection pool).
寫老式的ADO.NET程式的人都知道一定要這麼做 !

在ASP.NET MVC應用程式中, DbContext  實例基本上是在 Controller 中使用, 一些基礎知識說明 MVC 的 controllers 在當請求(request)到達時被建立, 然後在請求已經完成時被 dispose (are disposed), 在 ASP.NET MVC中如何確認 DbContext   實例被 dispose (is disposed)?



方法1 : 使用 Using 區塊

using (EmployeeContext context = new EmployeeContext())
{
    return View(context.Employees.ToList());
}

使用 using 區塊可以確保, 當執行到區塊底部時, EmployeeContext 會被 dispose (is disposed), using 區塊是 try{...}finally{...} 的簡寫方式(語法糖), context (DbContext 實例)會在 finally 裡面被 dispose (is disposed), 這區塊會確保 context 被 dispose (is disposed), 但是這樣很難在應用程式中的不同地方分享同一個 context, 會發現自己一直在建立更多要使用的 DbContext 實例, 這也使自己的 Controller 裡出現邏輯處理之外的雜訊, 即使比使用 try finally 乾淨多了, 但是依然感覺一些雜訊存在



方法2 : Dispose 區塊

另一種方法是在 controller 中實作 dispose

 public class EmployeeController : Controller

{
    private EmployeeContext _context;
 
    public EmployeeController()
    {
        _context = new EmployeeContext();
    }
        
    public ActionResult Index()
    {
        return View(_context.Employees.ToList());
    }
        
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _context.Dispose();
        }
        base.Dispose(disposing);
    }
}

當請求完成時, controller 會被dispose (is disposed), 這方式也確保了 EmployeeContext 也被dispose (is disposed); 和 using 區塊很像, 在Controller裡產生了雜訊並且很難在應用程式中不同的地方分享同一個實例, 這方法並且依賴團隊中的所有開發人員要正確的實作 dispose 

方法3 : 依賴注入 (Dependency Injection)

public class EmployeeController : Controller
{
    private EmployeeContext _context;
 
    public EmployeeController(EmployeeContext context)
    {
        _context = context;
    }
        
    public ActionResult Index()
    {
        return View(context.Employees.ToList());
    }
}

這方法解除了 controller 對 DbContext 實例的生命週期的責任. controller 要求一個 DbContext 實例, 但是不需要關心這實例從哪裡來或是當她結束的時候會去那裡?
我們知道這個 Controller 中只有一個建構子, 所以建立 EmployeeController 必須傳入一個 EmpolyeeContext 實例, 所以 Controller 不用再負責建立 DbContext, 意思說也不再需要去 dispose !
但是如果 Controller 不用再去建立 context, 那誰該去建立? 我們如何確認 context 真的被 dispose (is being disposed)?
用 IoC 容器解決這問題!
nuget上有很多 injection/IoC 容器, 例如 NInject, 大致流程是註冊使用 NInject 的 OnPerRequestHttpModule 並設置要用來產生實例的 DbContext(例如範例中的 EmpolyeeContext), 經過這些設置後, NInject 將會認出你的 Controller 所要求的實例(例如 EmpolyeeContext 的實例), 然後執行以下流程:
1. 每次 Http Request 時建立實例
2. 傳送實例到 Controller 建構子
3. Http Request 結束時 dipose 實例
OnPerRequestHttpModule 的預設行為:每次 Http Request 都建立一個新的 EmpolyeeContext 實例(context), 這表示不同的 Request 無法使用同一個 context, 也保證不會產生兩個以上的 EmpolyeeContext 被建立, 即使最後請求經過了三個都要求同一個 EmpolyeeContext 的 Controllers, 換句話說, context 的生命週期和 request 的生命週期綁在一起


2014年7月30日 星期三

取得更新失敗的詳細原因

1. 驗證不過的原因

public ActionResult Create(TopicCategory model)
{
    if (ModelState.IsValid)
    {
        _db.TopicCategory.Add(model);
        _db.SaveChanges();
        return Json(new { status = "success", information = model.Id });
    }
    else
    {
        string messages = string.Join("; ", ModelState.Values
                                .SelectMany(x => x.Errors)
                                .Select(x => x.ErrorMessage));
        return Json(new { status = "error", information = messages });
    }   
}  

2. EF SaveChanges()失敗原因

try
{
    _db.SaveChanges();
}
catch (DbEntityValidationException dbEx)
{
    foreach (var validationErrors in dbEx.EntityValidationErrors)
    {
        foreach (var validationError in validationErrors.ValidationErrors)
        {
            Trace.TraceInformation("Property: {0} Error: {1}", validationError.PropertyName, validationError.ErrorMessage);
        }
    }
}
or
try
{
    // Your code...
    // Could also be before try if you know the exception occurs in SaveChanges
    _db.SaveChanges();
}
catch (System.Data.Entity.Validation.DbEntityValidationException e)
{
    foreach (var eve in e.EntityValidationErrors)
    {
        Console.WriteLine("Entity of type \"{0}\" in state \"{1}\" has the following validation errors:",
            eve.Entry.Entity.GetType().Name, eve.Entry.State);
        foreach (var ve in eve.ValidationErrors)
        {
            Console.WriteLine("- Property: \"{0}\", Error: \"{1}\"",
                ve.PropertyName, ve.ErrorMessage);
        }
    }
    throw;
}

2014年7月20日 星期日

自定ASP.NET MVC前半段流程(路由)來理解ASP.NET MVC請求

背景 : 對HttpModule 、HttpHandler和HttpApplication管線有一定的了解

ASP.NET MVC 應用程式的啟動流程 : 

1、Application啟動時先通過RouteTable把URL映射到Handler

2、UrlRoutingModule(HttpModule)在PostResolveRequestCache事件中攔截用戶請求(在Init中註冊要攔截的事件),解析 request 並選取路由。


HttpModule 是註冊在 Web.config 中的,例如:
<configuration>
    <system.web>
        <httpModules>
            <!-- <add name="HelloWorldModule"
                      type="HelloWorldModule, HelloWorldModule" /> -->
        </httpModules>
    </system.web>
</configuration>


可是當打開Asp.net MVc 應用程式的 Web .Config 時卻沒有發現UrlRoutingModule的配置節,原因是:"它已經默認的寫在全局的中"。應此可以在“$\Windows\Microsoft.NET\Framework\版本號\Config\Web.config“ 中找到" <add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" /> ”


開始自定MVC流程
using System;
using System.Web;
using System.Web.Routing;
namespace Practice_MvcModule
{
    /// <summary>
    /// 實作 IHttpHandler 介面來自定 HttpHandler(類似 MvcHander)
    /// </summary>
    public class MyTestingMvcHandler : IHttpHandler
    {
        public MyTestingMvcHandler(RequestContext requestContext)
        {
 
            this.RequestContext = requestContext;
        }
 
        #region IHttpHandler 的成員
 
        public bool IsReusable
        {
            get { return true; }
        }
        // 啟用 HTTP Web 要求的處理 (Override the ProcessRequest method. )
        public void ProcessRequest(HttpContext context)
        {
            // 簡單地把路由訊息用文字輸出到頁面上 (應該需要對 Controller 加載, 激活並執行)
            context.Response.Write(String.Format("<h1>This is an HttpHandler Test.</h1><br/>{0} Controller and {1} action "
                , this.RequestContext.RouteData.Values["Controller"]
                , this.RequestContext.RouteData.Values["Action"]));
            context.Response.End();
        }
 
        #endregion
 
        public RequestContext RequestContext { getprivate set; }
    }
    /// <summary>
    /// 自定的處理比對路徑(類似 MvcRouteHandler)
    /// </summary>
    public class MyTestingRouteHandler : IRouteHandler
    {
        // 當路由被捕獲時, 返回一個 MyTestingMvcHandler
        public IHttpHandler GetHttpHandler(RequestContext requestContext)
        {
            return new MyTestingMvcHandler(requestContext);
        }
 
        IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext)
        {
            return this.GetHttpHandler(requestContext);
        }
    }
    /// <summary>
    /// 擴展 RouteCollection, 新增自訂方法
    ///     說明 : 
    ///         在Global.asax.cs 或 Route.Config.cs 的 RegisterRoutes 方法裡, RouteCollection 類使用的是 MapRoute 方法添加的路由, 
    ///         該方法是一個擴展方法, 它位於System.Web.Mvc 的 RouteCollectionExtensions 類中
    /// </summary>
    public static class MyTestingRouteCollectionExtensions
    {
        /// <summary>
        /// 對應指定的 URL 路由並設定預設路由值、條件約束和命名空間
        /// </summary>
        /// <param name="routes">應用程式的路由集合</param>
        /// <param name="name">要對應之路由的名稱</param>
        /// <param name="url">路徑的 URL 模式</param>
        /// <param name="defaults">包含預設路由值的物件</param>
        /// <returns></returns>
        public static Route MyTestingMapRoute(this RouteCollection routes, string name, string url, object defaults)
        {
            return MyTestingMapRoute(routes, name, url, defaults, nullnull);
        }
        /// <param name="constraints">為 url 參數指定值的一組運算式</param>
        /// <param name="namespaces">應用程式的命名空間集合</param>
        public static Route MyTestingMapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces)
        {
            if (routes == null)
            {
                throw new ArgumentNullException("routes");
            }
            if (url == null)
            {
                throw new ArgumentNullException("url");
            }
            // 註冊 Route 和 MyTestingRouteHandler 的映射關係
            Route route = new Route(url, new MyTestingRouteHandler())
            {
                Defaults = new RouteValueDictionary(defaults),
                Constraints = new RouteValueDictionary(constraints),
                DataTokens = new RouteValueDictionary()
            };
 
            if ((namespaces != null&& (namespaces.Length > 0))
            {
                route.DataTokens["Namespaces"= namespaces;
            }
 
            routes.Add(name, route);
 
            return route;
        }
    }
    /// <summary>
    /// 自訂的 HttpModule(類似 UrlRoutingModule)
    /// </summary>
    public class MyTestingHttpModule : IHttpModule
    {
        public void Init(HttpApplication context)
        {
            // 在 HttpApplication 管線抵達 PostMapRequestHandler 之前要先找到處理常式
            context.PostResolveRequestCache += new EventHandler(context_PostResolveRequestCache);
        }
 
        void context_PostResolveRequestCache(object sender, EventArgs e)
        {
            // 包裝目前的 HttpContext 物件
            HttpContextBase context = new HttpContextWrapper(((HttpApplication)sender).Context);
            // 將包裝後的 HttpContext 物件傳給 RouteTable, 透過要求參數在路表中比對路由物件, 然後回傳第一個符合的 RouteData 路由物件 (獲取路由訊息)
            RouteData routeData = RouteTable.Routes.GetRouteData(context);
 
            if (routeData == null)
            {
                return;
            }
            // 獲取 IRouteHandler 的實例 (例如 MvcRouteHandler, 或是自定的 MyTestingRouteHandler)
            IRouteHandler routeHandler = routeData.RouteHandler; 
            if (routeHandler == null)
            {
                throw new InvalidOperationException();
            }
            // 成功取得 RouteData 路由物件後, 建立表示目前 HttpContext 和 RouteData 的 RequestContext 物件 (建構請求上下文)
            RequestContext requestContext = new RequestContext(context, routeData);
            // 把 RequestContext 物件傳給 Handler 的建構式, 取得一個基於 RouteTable 的新 HttpHandler
            IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext);
            if (httpHandler == null)
            {
                throw new InvalidOperationException("無法建立對應的 HttpHandler 物件");
            }
            // 將 HttpHandler 實例映射到 HttpApplication 管線中
            context.RemapHandler(httpHandler);
 
        }
 
        public void Dispose()
        {
            throw new NotImplementedException();
        }
    }
 
 
}

新增路由規則
using System.Web.Mvc;
using System.Web.Routing;
namespace Practice_MvcModule
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
            routes.MyTestingMapRoute( // MyTestingMapRoute 為自行擴展 RouteCollection 的方法
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}

配置自定的 HttpModule
<configuration>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true">
      <add name="MyTestingHttpModule" type="Practice_MvcModule.MyTestingHttpModule"/>
      <!--
      <add name="MyTestingHttpModule" type="命名空間.類別名稱[,程式集名稱(因為直接在目錄中所以此範例可以不用指定)]"/>
      -->
    </modules>
  </system.webServer>
</configuration>

asp.net mvc 簡易流程說明:
UrlHttpModule的Init註冊了要攔截 PostResolveRequestCache 事件,在該事件中處理解析 request 並獲取 IRouteHandler 的實例 MvcRouteHandler (路由處理常式),根據 MvcRouteHandler 的 GetHttpHandler 方法獲取 IHttpHandler 的實例 MvcHandler,透過 MvcHandler 的 ProcessRequest 方法對 Controller 加載, 激活並執行。 

2014年7月16日 星期三

讓部分頁面(Partial View)使用類似 @section 的功能

因為部分頁面(Partial View)無法使用 @section 來將javascript或是css放置到 _Layout.cshtml 上指定的位置, 因此擴充 HtmlHelper 來達成相同功能

namespace HappyMovie.Web.Utilities.Helpers
{
    public static partial class HtmlRenderHelper{
        /// <summary>
        /// 讓部分頁面的 Javascript 可以加到 _Layout.chtml
        /// </summary>
        public static IHtmlString Resource(this HtmlHelper HtmlHelperFunc<objectHelperResult> Templatestring Type)
        {
            if (HtmlHelper.ViewContext.HttpContext.Items[Type!= null) ((List<Func<objectHelperResult>>)HtmlHelper.ViewContext.HttpContext.Items[Type]).Add(Template);
            else HtmlHelper.ViewContext.HttpContext.Items[Type= new List<Func<objectHelperResult>>() { Template };
 
            return new HtmlString(String.Empty);
        }
        public static IHtmlString RenderResources(this HtmlHelper HtmlHelperstring Type)
        {
            if (HtmlHelper.ViewContext.HttpContext.Items[Type!= null)
            {
                List<Func<objectHelperResult>> Resources = (List<Func<objectHelperResult>>)HtmlHelper.ViewContext.HttpContext.Items[Type];
 
                foreach (var Resource in Resources)
                {
                    if (Resource != nullHtmlHelper.ViewContext.Writer.Write(Resource(null));
                }
            }
 
            return new HtmlString(String.Empty);
        }
    }
}

在 _Layout.cshtml 上設定位置 :
@Html.RenderResources("css")
@Html.RenderResources("js"<!-- 自定的 HtmlHelper, 讓部分頁面的 Javascript 可以加到 _Layout.chtml -->

在部分頁面上使用 :
@Html.Resource(@<link rel="stylesheet" href="@Url.Content("~/Content/style.css")">"css")
@Html.Resource(@<style>
    .basic-data {
        floatleft;
    }
</style>"css")
@Html.Resource(
@<script>
    $(function () {

    });
</script>"js")


2014年5月10日 星期六

委拖(delegate)的協變與逆變

建立一個介面和一個實現介面的類別
internal interface IVehicle
{
    string Name { getset; }
}
 
internal class Car : IVehicle
{
    public string Name { getset; }
 
    public int Doors { getset; }
}

使用熟悉的類型進行演示
delegate T Func<out T>
delegate void Action<in T>(T obj)

使用 Lambda表達式可以很輕易地進行演示,甚至可以將他們連接起來
Func<Car> carFactory = () => new Car { Name = "BMW", Doors = 4 };
Func<IVehicle> vehicleFactory = carFactory; // 使用協變性轉換 Func<T>
 
Action<IVehicle> vehicleShow = vehicle => Console.WriteLine(vehicle.Name);
Action<Car> CarShow = vehicleShow; // 使用逆變性轉換 Action<T>
 
// 完整檢查
CarShow(carFactory()); // BMW
vehicleShow(vehicleFactory()); // BMW

協變性允許我們將汽車工廠視為更一般的載具(交通工具)工廠。
創建一個通用的行為,打印任何載具的名稱,使用逆變轉換,讓行為可用於人和載具。
最後將汽車工廠的結果提供給汽車展行為(action),將載具工廠的結果給載具秀行為,結果都是BMW

查看 vehicleFactor() 執行的結果
Console.WriteLine(vehicleFactory().Name);
Console.WriteLine(((Car)vehicleFactory()).Doors);
第一行顯示 BMW
第二行必須轉換類行為 Car 才能查看 Doors 屬性

介面(interface)的協變與逆變

介面(interface)的逆變與協變無法在 C# 3.0 進行編譯

定義一個介面和繼承該介面的兩個類別
internal interface IAnimal
{
    string Name { getset; }
 
    int Age { getset; }
}
 
internal class Dog : IAnimal
{
    public string Name { getset; }
 
    public int Age { getset; }
 
    public string Size { getset; }
}
 
internal class Bird : IAnimal
{
    public string Name { getset; }
 
    public int Age { getset; }
 
    public bool IsFlying { getset; }
}

實例化兩個List<T>物件
List<Dog> dogs = new List<Dog>
{
    new Dog{Name="Dog 1",Age=3, Size="Small"},
    new Dog{Name="Dog 2",Age=5, Size="Big"}
};
List<Bird> birds = new List<Bird>
{
    new Bird{Name="Bird 1",Age=4,IsFlying=true},
    new Bird{Name="Bird 2",Age=2,IsFlying=false}
};

演示 IEnumerable<out T>協變介面
1.
List<IAnimal> animals = new List<IAnimal>();
animals.AddRange(dogs);
animals.AddRange(birds);
建立一個 List<IAnimal>, 並調用 AddRange 向其添加 Dog 和 Bird列表(List);
List<T>.AddRange 的參數為 IEnumerable<T> 類型,
因此這種情況下,將這兩個列表都看成是 IEnumerable<IAnimal>,而這以前是不允許的。

2.
List<IAnimal> concat = dogs.Concat<IAnimal>(birds).ToList();
使用LINQ方法,根據已知序列的數據創建列表;
不能直接調用 dogs.Concat(birds),這會使類型推斷機制變得混亂, 應該顯示地指定類型參數。
dogs 和 cats 都將根據協變性而隱式轉換為 IEnumerable<IAnimal>, 這種轉換不會真正改變它們的值,所改變的只是編譯器如何看待這些值。

結果:
foreach (var item in animals)
{
    Console.WriteLine(item.Name);
}
顯示 :
Dog 1
Dog 2
Bird 1
Bird 2

無法使用 item.Size 或是 item.IsFlying, 除非轉為 Dog 或是 Bird:
foreach (var item in animal)
{
    if (item is Bird)
    {
        var bird = (Bird)item;
        Console.WriteLine("the bird is flying? {0}", bird.IsFlying);
    }
}

演示 IComparer<in T> 逆變介面
internal class AgeComparer : IComparer<IAnimal>
{
    public int Compare(IAnimal x, IAnimal y)
    {
        return x.Age.CompareTo(y.Age);
    }
}
自定 AgeComparer 類別,實現 IComparer<T>
....
IComparer<IAnimal> ageComparer = new AgeComparer();
dogs.Sort(ageComparer);
使用逆變性,進行排序;有了 IComparer<IAnimal>,就可以用她進行排序,dogs.Sort 的參數應該為 IComparer<Dog> 類型,但逆變性會進行隱式轉換。