2014年6月18日 星期三

MiniProfiler 3 + ASP.NET WebForms + ADO.NET

twMVC #15 的這一場研討會裡,我主講「開發的效能與效率」這一個主題,裡面所提到的 MiniProfiler 是我一直相當推薦給大家在開發的時候來使用,除了 ASP.NET MVC with Entity Framework 的專案可使用之外,甚至是 ASP.NET WebForms with Entity Framework 的專案也一樣可以使用,但如果在資料存取並非使用 Entity Framework  而是使用傳統的 ADO.NET 時應該怎麼設定與修改呢?

在這一篇裡將會以 ASP.NET WebForms + ADO.NET + MiniProfiler 3 的使用情境做使用與設定的說明。


我在之前的文章「MiniProfiler with ASP.NET WebForms」,雖然是有介紹在 ASP.NET WebForms 專案要使用 MiniProfiler 時應該怎麼設定,但是資料存取的部份卻依然是使用 Entity Framework。

而在 twMVC#10 所主講的「ASP.NET MVC Model 的設計與使用」也在其中的一個程式範例裡介紹到 ASP.NET WebForms 怎麼使用分層架構設計,當中就有 ASP.NET WebForms 專案使用 ADO.NET 並且也有加入 MiniProfiler,不過 MiniProfiler 的 Popup 視窗是有顯示,但是卻沒有顯示該頁面所執行的 SQL Command。

image

 

那麼在 MiniProfiler 官網上面針對一般 ADO.NET 的使用,是有以下的說明,

image

但是還有一些細節並沒有提到,所以下面就用個簡單的範例來說明。

 

開發環境:VS2013 Update 2, MS SQL Server 2014 Express, .NET Framework 4.5.1

建立一個基本的 ASP.NET WebForms 專案

image

專案使用的範例資料庫為 Northwind,在 Web.Config 裡加入 Connection String

image

接著在專案裡透過 NuGet 加入 MiniProfiler,只有這麼一個套件而已,沒有其他,

image

接著先在 Web.Config 的 system.webServer 區段裡加入 handler 內容,讓頁面顯示的 MiniProfiler Popup 視窗可以正常顯示,

image

<system.webServer>
    <handlers>
        <add name="MiniProfiler" path="mini-profiler-resources/*" verb="*" type="System.Web.Routing.UrlRoutingModule" resourceType="Unspecified" preCondition="integratedMode" />
    </handlers>
</system.webServer>

再來要在 Global.asax.cs 的 Application_BeginRequest 與 Application_EndRequeest 事件裡分別加入 MiniProfiler 的 Start 與 Stop 方法,

image

然後在 Site.Master 加入 MiniProfiler Popup 顯示的程式,建議是放在頁面的最下面,body 結尾標籤的上方,

image

 

這邊並不會直接在 ASPX 的 Code Behind 裡寫資料存取的程式,我會另外建立 Repository 類別的方式來做資料存取,所以在 Models 目錄裡,我建立了一些檔案,

image

Category.cs

image

BaseRepository.cs

這邊要特別說明,這裡要統一處理 DbConnection 的建立,而且為了要讓 MiniPrifiler 追蹤到 SQL Command 的內容,所以在 GetOpenConnection 方法裡回傳的是 DbConnection 而非 SqlConnection,另外也需要經過 StackExchange.Profiling.Data.ProfiledDbConnection() 方法來取得 DbConnection,

public abstract class BaseRepository
{
    private string _connectionString;
    public string ConnectionString
    {
        get { return _connectionString; }
        set { _connectionString = value; }
    }
 
    public BaseRepository()
    {
        this.ConnectionString = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
    }
 
    public BaseRepository(string connectionString)
    {
        if (!string.IsNullOrWhiteSpace(connectionString))
        {
            this._connectionString = connectionString;
        }
    }
 
    public DbConnection GetOpenConnection()
    {
        var cnn = CreateRealConnection();
        return new StackExchange.Profiling.Data.ProfiledDbConnection(cnn, MiniProfiler.Current);
    }
 
    private DbConnection CreateRealConnection()
    {
        var conn = new SqlConnection(this.ConnectionString);
        return conn;
    }
}

CategoryRepository.cs

CategoryRepository 繼承 BaseRepository,直接看裡面的 GetCategories() 與 GetOne() 方法的程式內容,

GetCategories 方法

DbConnection 是使用基礎類別的 GetOpenConnection 方法所取得,然後再經由 DbConnection 建立 DbCommand,然後分別指定 DbCommand 的 CommandType, CommnadTimeout, CommandText,最後是由 DbCommand 執行 ExecuteReader 傳回 DbDataReader,

public List<Category> GetCategories()
{
    var categories = new List<Category>();
 
    string sqlStatement = "select * from Categories order by CategoryID desc";
 
    using (var conn = this.GetOpenConnection())
    using (var command = conn.CreateCommand())
    {
        command.CommandType = CommandType.Text;
        command.CommandTimeout = 180;
        command.CommandText = sqlStatement;
 
        if (conn.State != ConnectionState.Open) conn.Open();
 
        using (IDataReader reader = command.ExecuteReader())
        {
            while (reader.Read())
            {
                var item = new Category
                {
                    CategoryID = int.Parse(reader["CategoryID"].ToString()), 
                    CategoryName = reader["CategoryName"].ToString(), 
                    Description = reader["Description"].ToString()
                };
 
                categories.Add(item);
            }
        }
    }
    return categories;
}

GetOne 方法

同樣的,GetOne 方法的處理也是一樣,而 Parameter 的部份,可以使用 DbCommand.CreateParameter() 方法,或是使用 command.Parameter.Add() 方法去加入 SqlParameter 也是可以,

/// <summary>
/// Gets the one.
/// </summary>
/// <param name="id">The id.</param>
/// <returns></returns>
public Category GetOne(int id)
{
    string sqlStatement = "select * from Categories where CategoryID = @CategoryID";
 
    var item = new Category();
 
    using (var conn = this.GetOpenConnection())
    using (var command = conn.CreateCommand())
    {
        command.CommandType = CommandType.Text;
        command.CommandTimeout = 180;
        command.CommandText = sqlStatement;
 
        var param = command.CreateParameter();                
        param.ParameterName = "CategoryID";
        param.Value = id;
        command.Parameters.Add(param);
 
        //command.Parameters.Add(new SqlParameter("CategoryID", id));
 
        if (conn.State != ConnectionState.Open) conn.Open();
 
        using (IDataReader reader = command.ExecuteReader())
        {
            if (reader.Read())
            {
                item.CategoryID = int.Parse(reader["CategoryID"].ToString());
                item.CategoryName = reader["CategoryName"].ToString();
                item.Description = reader["Description"].ToString();
            }
        }
    }
    return item;
}

 

顯示頁面

image

這裡就簡單建立兩個頁面,分別是 Details.aspx 與 List.aspx,List.aspx 是顯示全部的 Category 資料,而 Details.aspx 是顯示單一個指定 Category 的資料,下面就直接看 Code Behind 的程式內容,

List.aspx.cs

public partial class List : System.Web.UI.Page
{
    private CategoryRepository _repository = new CategoryRepository();
    
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!this.IsPostBack)
        {
            this.CategoryDataBind();
        }
    }
 
    private void CategoryDataBind()
    {
        var categories = this._repository.GetCategories();
        this.GridView1.DataSource = categories;
        this.GridView1.DataKeyNames = new string[] { "CategoryID" };
        this.GridView1.DataBind();
    }
 
    protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
    {
        if (e.Row.RowType == DataControlRowType.DataRow)
        {
            object primaryKey = GridView1.DataKeys[e.Row.RowIndex]["CategoryID"];
            HyperLink HyperLink_Details = e.Row.FindControl("HyperLink_Details") as HyperLink;
 
            int categoryID;
            if (int.TryParse(primaryKey.ToString(), out categoryID)
                && HyperLink_Details != null)
            {
                HyperLink_Details.NavigateUrl = string.Concat("Details.aspx?id=", categoryID.ToString());
            }
        }
    }
}

Details.aspx.cs

public partial class Details : System.Web.UI.Page
{
    private CategoryRepository _repository = new CategoryRepository();
 
    private int categoryID;
    public int CategoryID
    {
        get { return categoryID; }
        set { categoryID = value; }
    }
 
    private Category instance;
    public Category Instance
    {
        get
        {
            if (this.instance == null)
            {
                var category = this._repository.GetOne(this.CategoryID);
                this.instance = category;
            }
            return instance;
        }
    }
 
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!Page.IsPostBack)
        {
            this.SetDefault();
        }
    }
 
    private void SetDefault()
    {
        int id;
        if (Request.QueryString["id"] == null)
        {
            Response.Redirect("List.aspx");
        }
        else
        {
            if (!int.TryParse(Request.QueryString["id"].Trim(), out id))
            {
                Response.Redirect("List.aspx");
            }
            else
            {
                this.CategoryID = id;
                if (this.Instance == null)
                {
                    Response.Redirect("List.aspx");
                }
                else
                {
                    this.Label_CategoryID.Text = this.Instance.CategoryID.ToString();
                    this.TextBox_CategoryName.Text = this.Instance.CategoryName;
                    this.TextBox_Description.Text = this.Instance.Description;
                }
            }
        }
    }
}

 

執行結果

List.aspx

image

image

Details.aspx

image

image

 

Paramerer Value

上面的圖片裡雖然可以看到 MiniProfiler 有將執行的 SQL Command 給顯示出來,但是卻沒有顯示 Parameter 的值,如果一個頁面裡有執行多個 SQL Command 的時候,也是蠻需要知道傳進 SQL Command 裡的 Parameter Value 是什麼,要在 MiniProfiler Popup 視窗裡顯示 Parameter Value 只需要簡單的設定,

image

執行結果

image

 


參考連結

sql server - Using mvc-mini-profiler with ADO.NET SqlConnection - Stack Overflow

mvc mini profiler - Can the ServiceStack MiniProfiler show SQL parameter values, not just the bound parameter names? - Stack Overflow

 

以上

5 則留言:

  1. 你好
    大大的文章寫的很棒,可是若像我是使用 linq to sql的方式做的話,(會有dbml)的檔案
    那要怎麼錄sql語法呢?
    有參考官網,但官網的範例寫的不是很懂
    不知道大大是否可以點解要怎麼做linq to sql的錄製?

    回覆刪除
    回覆
    1. 「有參考官網,但官網的範例寫的不是很懂」???

      刪除
    2. 我自己有找到方法解決了,不好意思,打擾了

      刪除

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。