[MVC]如何使ASP.NET MVC Controller易测试呢?

mikel阅读(855)

昨天那篇文章如何进行ASP.NET MVC 的测试,虽然通过自己写一个Fake的HttpContext,但是同时也暴露出之所以难于测试,是因为设计的代码不易测试,根据Jeffrey Zhao(老赵)的建议(我没完全看懂如何去做),我又重新试了一下,我把对httpcontext的操作全部放到一个ModelBinder里,因为那里可以处理httpcontext,而测试的时候是不执行ModelBinder

下面是实验的代码

Product类

image

ProductBinder

image

HomeController

image

记得在global.asax里注册

image

测试代码

image

很明显,测试是通过的,因为此时Action没有用到任何httpcontext的内容,处理的只是传进去的Product对象。

现在我的问题如下:

1. 很明显About的Action是通不过测试的,这里我只是使用了一个session值,难道我也需要新建一个ModerBinder,和一个类吗?这样 凡是出现调用httpcontext都的新建一个Binder那Binder是不是太多了,ModelBinder是不是主要处理Form?

2. 如果不用ModelBinder怎么样即可以使用session又易测呢?

3. 虽然Action好测了,但ProductBinder如何测?

注:本文没有技术含量,为何放在首页?因为我没有搜索到相关的内容,而园子里很多人肯定有更好的方案,但是他们的时间可能很紧,我想他们能在评论里给点好的思路,文章+评论=一篇好文, 我这里就是想抛砖引玉,把他们的经验为大家引出来。

[C#]独立主机上实现二级别域名解释(泛解释) (asp.net)

mikel阅读(748)

1\那就是把 *.iloveyou.io解释到服务器默认网站

2\在项目Global的事件protected void Application_BeginRequest(Object sender, EventArgs e)中通过

   string _path = Request.Url.AbsoluteUri;

   读取当前路径,然后做处理就是了!

   把

   _path="http://liangsan.iloveyou.io/ "

   的路径变为

  newurl="love/?toName=liangsan"

然后通过

    HttpContext.Current.RewritePath(newurl);

 就搞定了!就这么简单!

[MVC]如何进行ASP.NET MVC 的测试

mikel阅读(796)

如何进行ASP.NET MVC 的测试

本文参考了http://stephenwalther.com/blog/的内容。

今天需要对ASP.NET MVC的Controller进行测试,我们都知道当我们在测试工程里new一个controller时,这个controller里的httpcontext是空的,也就是session,cookie, form等都是空。

方法一:Mock controller的HttpContext, 暂时失败

那么我们如何对controller进行测试呢,我首先想到的是mock一个httpcontext,这里我用的是Rhino Mocks

public static class MvcMockHelpers

    {

        public static HttpContextBase FakeHttpContext(this MockRepository mocks)

        {

            HttpContextBase context = mocks.PartialMock<HttpContextBase>();

            HttpRequestBase request = mocks.PartialMock<HttpRequestBase>();

            HttpResponseBase response = mocks.PartialMock<HttpResponseBase>();

            HttpSessionStateBase session = mocks.PartialMock<HttpSessionStateBase>();

            HttpServerUtilityBase server = mocks.PartialMock<HttpServerUtilityBase>();

 

            SetupResult.For(context.Request).Return(request);

            SetupResult.For(context.Response).Return(response);               

            SetupResult.For(context.Session).Return(session);

            SetupResult.For(context.Server).Return(server);

 

            mocks.Replay(context);

            return context;

        }

 

        public static HttpContextBase FakeHttpContext(this MockRepository mocks, string url)

        {

            HttpContextBase context = FakeHttpContext(mocks);

            context.Request.SetupRequestUrl(url);

            return context;

        }

 

        public static void SetFakeControllerContext(this MockRepository mocks, Controller controller)

        {

            var httpContext = mocks.FakeHttpContext();

            ControllerContext context = new ControllerContext(new RequestContext(httpContext, new RouteData()), controller);

            controller.ControllerContext = context;

        }

下面我们建立一个ASP.NET MVC工程

using System;

using System.Collections.Generic;

using System.Linq;

using System.Web;

using System.Web.Mvc;

using System.Web.Mvc.Ajax;

 

namespace MVCTest.Controllers

{

    [HandleError]

    public class HomeController : Controller

    {

        public ActionResult TestSession()

        {

            Session["name"] = "Jack Wang";

            ViewData["Title"] = "Home Page";

            ViewData["Message"] = "Welcome to ASP.NET MVC!";

            if (Session["CurrentCulture"] != null)

            {

                ViewData["CurrentCulture"] = Session["CurrentCulture"];

            }

            return View();

        }

 

        public ActionResult TestForm()

        {

            ViewData["Name"] = Request.Form["Name"];

            ViewData["Age"] = Request.Form["Age"];

            ViewData["count"] = Request.Form.Count;

            return View();

        }

 

        public ActionResult TestLogin()

        {

            if (User.Identity.IsAuthenticated)

            {

                ViewData["userName"] = User.Identity.Name;

                return View("Admin");

            }

            else

            {

                return RedirectToAction("Index");

            }

        }

 

        public ActionResult Admin()

        {

            if (User.IsInRole("Admin"))

            {

                return View("Admin");

            }

            else

            {

                return RedirectToAction("Index");

            }

        }

 

        public ViewResult Details()

        {

            ViewData["PageSize"] = Request.QueryString["PageSize"];

            ViewData["CurrentPage"] = Request.QueryString["CurrentPage"];

            ViewData["count"] = Request.QueryString.Count;

 

            return View();

        }

        public ViewResult TestCookie()

        {

            ViewData["key"] = Request.Cookies["key"].Value;

            return View();

        }

    }

}

 

测试代码

 

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Web.Mvc;

using Microsoft.VisualStudio.TestTools.UnitTesting;

using MVCTest;

using MVCTest.Controllers;

using MvcFakes;

using System.Web.SessionState;

using System.Web;

using System.Collections.Specialized;

using Rhino.Mocks;

namespace MVCTest.Tests.Controllers

{

    /// <summary>

    /// Summary description for HomeControllerTestByMock

    /// </summary>

    [TestClass]

    public class HomeControllerTestByMock

    {

        public HomeControllerTestByMock()

        {

            //

            // TODO: Add constructor logic here

            //

        }

        private MockRepository mocks;

        [TestInitialize]

        public void Setup()

        {

            mocks = new MockRepository();

        }

        [TestMethod]

        public void TestSession()

        {

            // Arrange

 

            HomeController controller = new HomeController();

            mocks.SetFakeControllerContext(controller);

            controller.Session.Add("name", "Jack Wang");

            SetupResult.For(controller.Session["CurrentCulture"]).Return("zh-CN");

 

            mocks.ReplayAll();

            // Act

            ViewResult result = controller.TestSession() as ViewResult;

 

            // Assert

            ViewDataDictionary viewData = result.ViewData;

            Assert.AreEqual("Home Page", viewData["Title"]);

            Assert.AreEqual("Welcome to ASP.NET MVC!", viewData["Message"]);

            Assert.AreEqual(controller.Session["name"], "Jack Wang");

            Assert.AreEqual("zh-CN", viewData["CurrentCulture"]);

        }

 

    }

}

运行,测试

image

从错误信息可以看到是因为代码里Session["name"] = "Jack Wang"出错

本人排查很久,只知道mock的controllercontext的Session里没有这个Key,但现在还没有找到如何解决,哪位高人能帮吗解决?

 

二,自己写个模拟的Fake类,测试通过

 

既然这种方法不行,我们只能换一种方法,还好controller的ControllerContext可以赋值,这样我们就通过继承ControllerContext来Fake

using System;

using System.Collections;

using System.Collections.Generic;

using System.Collections.Specialized;

using System.Linq;

using System.Text;

using System.Web;

using System.Web.SessionState;

 

namespace MvcFakes

{

    public class FakeHttpSessionState : HttpSessionStateBase

    {

        private readonly SessionStateItemCollection _sessionItems;

 

        public FakeHttpSessionState(SessionStateItemCollection sessionItems)

        {

            _sessionItems = sessionItems;

        }

 

        public override void Add(string name, object value)

        {

            _sessionItems[name] = value;

        }

 

        public override int Count

        {

            get

            {

                return _sessionItems.Count;

            }

        }

 

        public override IEnumerator GetEnumerator()

        {

            return _sessionItems.GetEnumerator();

        }

 

        public override NameObjectCollectionBase.KeysCollection Keys

        {

            get

            {

                return _sessionItems.Keys;

            }

        }

 

        public override object this[string name]

        {

            get

            {

                return _sessionItems[name];

            }

            set

            {

                _sessionItems[name] = value;

            }

        }

 

        public override object this[int index]

        {

            get

            {

                return _sessionItems[index];

            }

            set

            {

                _sessionItems[index] = value;

            }

        }

 

        public override void Remove(string name)

        {

            _sessionItems.Remove(name);

        }

    }

}

 

using System;

using System.Collections.Specialized;

using System.Web;

 

namespace MvcFakes

{

 

    public class FakeHttpRequest : HttpRequestBase

    {

        private readonly NameValueCollection _formParams;

        private readonly NameValueCollection _queryStringParams;

        private readonly HttpCookieCollection _cookies;

 

        public FakeHttpRequest(NameValueCollection formParams, NameValueCollection queryStringParams, HttpCookieCollection cookies)

        {

            _formParams = formParams;

            _queryStringParams = queryStringParams;

            _cookies = cookies;

        }

 

        public override NameValueCollection Form

        {

            get

            {

                return _formParams;

            }

        }

 

        public override NameValueCollection QueryString

        {

            get

            {

                return _queryStringParams;

            }

        }

 

        public override HttpCookieCollection Cookies

        {

            get

            {

                return _cookies;

            }

        }

 

    }

}

 

using System;

using System.Security.Principal;

 

namespace MvcFakes

{

 

 

    public class FakeIdentity : IIdentity

    {

        private readonly string _name;

 

        public FakeIdentity(string userName)

        {

            _name = userName;

 

        }

 

        public string AuthenticationType

        {

            get { throw new System.NotImplementedException(); }

        }

 

        public bool IsAuthenticated

        {

            get { return !String.IsNullOrEmpty(_name); }

        }

 

        public string Name

        {

            get { return _name; }

        }

 

    }

 

 

}

 

using System;

using System.Linq;

using System.Security.Principal;

 

namespace MvcFakes

{

 

    public class FakePrincipal : IPrincipal

    {

        private readonly IIdentity _identity;

        private readonly string[] _roles;

 

        public FakePrincipal(IIdentity identity, string[] roles)

        {

            _identity = identity;

            _roles = roles;

        }

 

        public IIdentity Identity

        {

            get { return _identity; }

        }

 

        public bool IsInRole(string role)

        {

            if (_roles == null)

                return false;

            return _roles.Contains(role);

        }

    }

}

 

using System;

using System.Collections.Specialized;

using System.Security.Principal;

using System.Web;

using System.Web.SessionState;

 

namespace MvcFakes

{

    public class FakeHttpContext : HttpContextBase

    {

        private readonly FakePrincipal _principal;

        private readonly NameValueCollection _formParams;

        private readonly NameValueCollection _queryStringParams;

        private readonly HttpCookieCollection _cookies;

        private readonly SessionStateItemCollection _sessionItems;

 

        public FakeHttpContext(FakePrincipal principal, NameValueCollection formParams, NameValueCollection queryStringParams, HttpCookieCollection cookies, SessionStateItemCollection sessionItems )

        {

            _principal = principal;

            _formParams = formParams;

            _queryStringParams = queryStringParams;

            _cookies = cookies;

            _sessionItems = sessionItems;

        }

 

        public override HttpRequestBase Request

        {

            get

            {

                return new FakeHttpRequest(_formParams, _queryStringParams, _cookies);

            }

        }

 

        public override IPrincipal User

        {

            get

            {

                return _principal;

            }

            set

            {

                throw new System.NotImplementedException();

            }

        }

 

        public override HttpSessionStateBase Session

        {

            get

            {

                return new FakeHttpSessionState(_sessionItems);

            }

        }

 

    }

}

 

using System;

using System.Collections.Specialized;

using System.Web;

using System.Web.Mvc;

using System.Web.Routing;

using System.Web.SessionState;

 

namespace MvcFakes

{

 

    public class FakeControllerContext : ControllerContext

    {

        public FakeControllerContext(ControllerBase controller)

            : this(controller, null, null, null, null, null, null)

        {

        }

 

        public FakeControllerContext(ControllerBase controller, HttpCookieCollection cookies)

            : this(controller, null, null, null, null, cookies, null)

        {

        }

 

        public FakeControllerContext(ControllerBase controller, SessionStateItemCollection sessionItems)

            : this(controller, null, null, null, null, null, sessionItems)

        {

        }

 

 

        public FakeControllerContext(ControllerBase controller, NameValueCollection formParams)

            : this(controller, null, null, formParams, null, null, null)

        {

        }

 

 

        public FakeControllerContext(ControllerBase controller, NameValueCollection formParams, NameValueCollection queryStringParams)

            : this(controller, null, null, formParams, queryStringParams, null, null)

        {

        }

 

 

 

        public FakeControllerContext(ControllerBase controller, string userName)

            : this(controller, userName, null, null, null, null, null)

        {

        }

 

 

        public FakeControllerContext(ControllerBase controller, string userName, string[] roles)

            : this(controller, userName, roles, null, null, null, null)

        {

        }

 

 

        public FakeControllerContext

            (

                ControllerBase controller,

                string userName,

                string[] roles,

                NameValueCollection formParams,

                NameValueCollection queryStringParams,

                HttpCookieCollection cookies,

                SessionStateItemCollection sessionItems

            )

            : base(new FakeHttpContext(new FakePrincipal(new FakeIdentity(userName), roles), formParams, queryStringParams, cookies, sessionItems),

            new RouteData(), controller)

        { }

    }

}

 

下面是测试类

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Web.Mvc;

using Microsoft.VisualStudio.TestTools.UnitTesting;

using MVCTest;

using MVCTest.Controllers;

using MvcFakes;

using System.Web.SessionState;

using System.Web;

using System.Collections.Specialized;

namespace MVCTest.Tests.Controllers

{

    /// <summary>

    /// Summary description for HomeControllerTest

    /// </summary>

    [TestClass]

    public class HomeControllerTest

    {

        [TestMethod]

        public void TestSession()

        {

            // Arrange

            HomeController controller = new HomeController();

            var sessionItems = new SessionStateItemCollection();

            sessionItems["CurrentCulture"] = "zh-CN";

            controller.ControllerContext = new FakeControllerContext(controller, sessionItems);

            // Act

            ViewResult result = controller.TestSession() as ViewResult;

 

            // Assert

            ViewDataDictionary viewData = result.ViewData;

            Assert.AreEqual("Home Page", viewData["Title"]);

            Assert.AreEqual("Welcome to ASP.NET MVC!", viewData["Message"]);

            Assert.AreEqual(sessionItems["name"], "Jack Wang");

            Assert.AreEqual("zh-CN", viewData["CurrentCulture"]);

        }

        [TestMethod]

        public void TestFakeFormParams()

        {

 

            var controller = new HomeController();

 

            var formParams = new NameValueCollection { { "Name", "Jack" }, { "Age", "28" } };

            controller.ControllerContext = new FakeControllerContext(controller, formParams);

 

            var result = controller.TestForm() as ViewResult;

            Assert.AreEqual("Jack", result.ViewData["Name"]);

            Assert.AreEqual("28", result.ViewData["Age"]);

            Assert.AreEqual(formParams.Count, result.ViewData["count"]);

        }

 

        [TestMethod]

        public void TestFakeQueryStringParams()

        {

            var controller = new HomeController();

 

            var queryStringParams = new NameValueCollection { { "PageSize", "10" }, { "CurrentPage", "5" } };

            controller.ControllerContext = new FakeControllerContext(controller, null, queryStringParams);

 

            var result = controller.Details() as ViewResult;

            Assert.AreEqual("10", result.ViewData["PageSize"]);

            Assert.AreEqual("5", result.ViewData["CurrentPage"]);

            Assert.AreEqual(queryStringParams.Count, result.ViewData["count"]);

        }

 

        [TestMethod]

        public void TestFakeUser()

        {

            var controller = new HomeController();

 

            controller.ControllerContext = new FakeControllerContext(controller, "Jack Wang");

            var result = controller.TestLogin() as ActionResult;

            Assert.IsInstanceOfType(result, typeof(ViewResult));

            ViewDataDictionary viewData = ((ViewResult)result).ViewData;

            Assert.AreEqual("Jack Wang", viewData["userName"]);

 

 

            controller.ControllerContext = new FakeControllerContext(controller);

            result = controller.TestLogin() as ActionResult;

            Assert.IsInstanceOfType(result, typeof(RedirectToRouteResult));

        }

 

 

        [TestMethod]

        public void TestFakeUserRoles()

        {

 

            var controller = new HomeController();

 

 

            controller.ControllerContext = new FakeControllerContext(controller, "Jack Wang", new string[] { "Admin" });

            var result = controller.Admin() as ActionResult;

            Assert.IsInstanceOfType(result, typeof(ViewResult));

 

 

            controller.ControllerContext = new FakeControllerContext(controller);

            result = controller.Admin() as ActionResult;

            Assert.IsInstanceOfType(result, typeof(RedirectToRouteResult));

        }

 

 

 

        [TestMethod]

        public void TestCookies()

        {

 

            var controller = new HomeController();

 

 

            var cookies = new HttpCookieCollection();

            cookies.Add(new HttpCookie("key", "a"));

            controller.ControllerContext = new FakeControllerContext(controller, cookies);

            var result = controller.TestCookie() as ViewResult;

 

 

            Assert.AreEqual("a", result.ViewData["key"]);

        }

    }

}

测试完全通过

image 

 

总结: 个人感觉ASP.NET MVC的测试是很不友好的,和ruby on rails相比,在测试这一块还有很大的距离,希望正式版里这一块能够加强

 

王德水 写于2008-01-05 00:30分

 

本文源码下载

[Javascript]javascript中setTimeout 和setInterval的区别

mikel阅读(674)

window对象有两个主要的定时方法,分别是setTimeout 和 setInteval 他们的语法基本上相同,但是完成的功能取有区别。
  setTimeout方法是定时程序,也就是在什么时间以后干什么。干完了就拉倒。
  setInterval方法则是表示间隔一定时间反复执行某操作。

  如果用setTimeout实现setInerval的功能,就需要在执行的程序中再定时调用自己才行(setTimeout( ) 預設只是執行一次, 但我們可以使用一個循環方式, 使到一個setTimeout( ) 再啟動自己一次, 就會使到第二個 setTimeout( ) 執行, 第二個又啟動第三個, 這樣循環下去, 這 setTimeout( ) 就會不斷執行)。如果要清除计数器需要根据使用的方法不同,调用不同的清除方法:

  1. var timeID = null;   
  2.   
  3. //timeout方法   
  4. timeID=setTimeout('northsnow()',1000);   
  5. clearTimeout(timeID);     
  6.   
  7. //interval方法   
  8. timeID=setInterval('northsnow()',1000);   
  9. clearInteval(timeID);  

 eg:

  1. <script type="text/JavaScript">   
  2. var count = 0;   
  3. var timeID = null;   
  4. function counter() {   
  5.     count++;       
  6.     $("#count").html(count + '%');   
  7.     timeID = setTimeout("counter()"1000);//注意:执行的函数需要加引号,否则会报错的
  8.     if (count > 10) {   
  9.         clearTimeout(timeID);   //注意:clearTimeout(timeID)必须位于setTimeout()之后,不能是之前,原因很容易呦!   
  10.     }   
  11. }   
  12. $(function() {   
  13.     counter();   
  14. });   
  15. </script>  

 

  1. <script type="text/JavaScript">   
  2. var count = 0;   
  3. var timeID = null;   
  4. function counter() {   
  5.     count++;       
  6.     $("#count").html(count + '%');   
  7.     if (count > 10) {   
  8.         clearInterval(timeID);   
  9.     }       
  10. }   
  11. $(function() {       
  12.     timeID = setInterval('counter()', 1000);  //注意:执行的函数需要加引号,否则会报错的 
  13.        
  14. });   
  15. </script>  

    好了,基本原理明白了,剩下的就是自己灵活运用了,可以开发一些定时器或延时器之类的东东^_^.

[Javascript]关闭浏览器的事件

mikel阅读(867)

在做图片刷新功能的时候,会出现IE图片缓存的问题,试了N多方法都没有效果,最后只有使用每刷新一次修改图片文件名加随机数的方法才勉强解决。但是这样 的方法是下次打开浏览器读取图片的时候,上次产生的随机数是动态的,这时候只有让用户关闭浏览器的时候捕捉关闭事件然后将图片更名为一个统一的名字。由于 浏览器是无状态的,在这时候捕捉浏览器关闭会出现两种情况:1.真正的关闭浏览器 2.刷新浏览器。如何判断区分这两种动作呢。
一. JavaScript代码处理方法:
      function window.onbeforeunload()  
      {        
        //用户点击浏览器右上角关闭按钮
        if(event.clientX>document.body.clientWidth&&event.clientY<0||event.altKey)  
        {   
                document.getElementById("btnCompelete").click();
//              window.event.returnValue="确定要退出本页吗?";  
        }
        //用户点击任务栏,右键关闭
        else if(event.clientY > document.body.clientHeight || event.altKey)
        {
            document.getElementById("btnCompelete").click();
//            window.event.returnValue="确定要退出本页吗?";  
        }
         else//其他情况为刷新   
         {   
              alert("你在刷新");
         }  
      } 
其中 event.clientX   鼠标光标X坐标     document.body.clientWidth窗体工作区宽度     event.clientY鼠标光标Y坐标     event.altKey   是否按下alt键
二. 事件捕捉方法:
<body scroll="no" onbeforeunload="return CloseEvent();" onunload="UnLoadEvent()" >
</body>

<script language="JavaScript" type="text/JavaScript">
 
  
    var DispClose = true;
    function CloseEvent()
    {
        if (DispClose)
        {
            return "是否离开当前页面?";
        }
    }
   
    function UnLoadEvent()
    {
        DispClose = false;
        //在这里处理关闭页面前的动作
    }
在页面卸载之前引发onbeforeunload事件,如果用户选择“是”即确定卸载页面将引发onunload事件,否则返回页面不做任何操作。

[C#]static变量与asp.net

mikel阅读(973)

 在C#中,static变量表示该变量属于类,而不是类的实例。可以说是该类的所有实例共享一个static变量。
ASP.NET的页面就是一个类,我们访问一个页面。就会在服务器上实例化一个该类的实例,来响应我们的请求。
“所有实例共享一个static变量” 这就意味着,所有的客户端访问到的ASP.NET页面中static变量都是同一个变量。

由于我们每次访问asp.net页面都是一个全新的对象,而不是我们上一次访问的对象。所以上次页面访问时我们对页面中变量的改动都没有保留。遇到 这个问题的时候,很多初学者的直觉就是将这个变量申明为static,自己在测试的时候发现还真的保留住了页面的状态。窃喜之余没有发现这又有引入了另外 一个错误。因为你要的只是页面能保留住状态,而这个状态是针对一个客户端的(session的效果)。而得到的结果是只要一个客户端改变了该值所有的其他 客户端都受到了影响(如同Applicatin的效果)。这种情况下,需要的极有可能就是个ViewState或者是Session。

Application与static变量
Application是通过一个集合保存所有的对象。

强类型:
Application中保存的是object,对对象的保存和使用需要作cast动作。对于值类型更需要Box&UnBox。对性能的影响较大。
而static变量是强类型的对象。

线程同步:
Application将所有的对象放到一个集合,这样对访问集合中的任何对象都会锁定这个集合。
假如有Application["A"]、Application["B"]、Application["C"],有线程访问Application["A"]其他线程不能访问Application["B"] and Application["C"]。
而static变量,可以根据他们的作用分别放在不同的class当中。这样可以并行访问不同的static变量,而不存在线程安全问题。

友情提示:
1. 对static变量,做lock时。可以通过lock(typeof(classname))来锁定该变量所在的类的类型,达到线程同步的目的。
2. 由于Aplication,static member是全局变量,而我们是在多线程服务器环境写程序,对他们的使用需要注意线程安全的问题

[MVC]ASP.Net MVC Framework - Create your own IRout

mikel阅读(839)

In my previous post I wrote about how we can create our own IControllerFactory for the ASP.NET MVC Framework; in this post I will show you how we can create our own IRouteHandler.
The RouteHandler in this post will replace the IControllerFactory with my own Controller Factory, and also set a default ControllerFactory and a ViewFactory specified in the web.config. This is something the current preview bits of the MVC Framework can’t.
It’s quite easy to create our own RouteHandler, we only need to implement the IRouteHandler interface and implement the GetHttpHandler method 😉
public class N2MVCRouteHandler : IRouteHandler
{
   public IHttpHandler GetHttpHandler(RequestContext requestContext)
   {
       N2MVCHandler handler = new N2MVCHandler();
       handler.RequestContext = requestContext;
       return handler;
    }
}

The GetHttpHandler returns an IHttpHandler. I have created my own IHttpHandler (N2MVCHandler). The N2MVCHandler inherits the MvcHandler shipped with the ASP.NET MVC Framework. The reason why I inherit the MvcHandler is because I don’t want to rewrite stuff that isn’t necessarily. In my IHttpHandler I override the ProcessRequest method and added my own code to create a Controller for the current request.

public class N2MVCHandler : MvcHandler
{
    protected override void ProcessRequest(IHttpContext httpContext)
    {
       if (this.RequestContext == null)
          throw new InvalidOperationException("No RequestContext");

       string controllerName = this.GetRequiredString(this.RequestContext.RouteData, "controller");

       IController controller = this.CreateController(controllerName);
       ControllerContext controllerContext = new ControllerContext(base.RequestContext, controller);

       controller.Execute(controllerContext);
    }

    private IController CreateController(string controllerName)
    {
       N2MVCConfigurationHandler config = ConfigurationManager.GetSection("Nsquared2/N2MVCSection") as N2MVCConfigurationHandler;

       IN2ControllerFactory controllerFactory = N2ControllerFactoryBuilder.GetControllerFactory(config.ControllerFactory);

       IController controller = controllerFactory.CreateController(base.RequestContext, controllerName);

       return controller;
    }
}

In the ProcessRequest method I also create a Controller for the current request by calling the CreateController method. The CreateController method in the N2MvcHandler instantiates a ControllerFactroy which is specified in the web.config file. When the factory is instantiated I make a call to its CreateController method to create an instance of the Controller.

In this example I have replace the IControllerFactory with my own interface, the reason is that I want to pass the name of the Controller and not the Type. The MvcHandler shipped with the MVC Framework will in the ProcessRequest method try to locate the Controller within the references assemblies and create the Type of the Controller and pass it to the IController.CreateController method. But I think it’s up to the ControllerFactory to look up the Controller. Here is my IControllerFactory interface:

public interface IN2ControllerFactory
{
    IController CreateController(RequestContext context, string controllerName);
}

Note: You can still create your own ControllerFactory, but you need to implement my interface instead of IControllerFactory, and you don’t need to make a call to the ControllerBuilder.Current.SetDefaultControllerFactory method in the Application_Start event in Global.asax to specify which ControllerFactory you want to use, instead you can do it in web.config. You will see how to do it later in this post.

I decided to use Spring.Net in this post also to create my Controller within the ControllerFactory.

public class N2ControllerFactory : IN2ControllerFactory
{
    public IController CreateController(RequestContext context, string controllerName)
    {
        IResource input = new FileSystemResource(context.HttpContext.Request.MapPath("objects.xml"));
        IObjectFactory factory = new XmlObjectFactory(input);

        IController controller = (IController)factory.GetObject(controllerName, typeof(IController));

        if (typeof(Controller).IsAssignableFrom(controller.GetType()))
        {
          N2MVCConfigurationHandler config = ConfigurationManager.GetSection("Nsquared2/N2MVCSection") as N2MVCConfigurationHandler;
          ((Controller)controller).ViewFactory = N2ViewFactoryBuilder.GetViewFactory(config.ViewFactory);
        }
        return controller;
    }
}
It's the ControllerFactory's responsibility to create and fill the Controller with all it information it needs. In this case the ViewFactory.

The IViewFactory is used to create a factory which has the responsibility to create a View. Because a Controller don’t need to implement the Controller base class I will in my code use a "code policy". I will check if the created Controller inherits the Controller class, if so I will create a IViewFactory and inject it to the Controller. The IController interface don’t have the ViewFactory property, it’s something we will get from the Controller base class. 

If we take a look again at the CreateContoller method in the IHttpHandler (N2MVCHandler), we can see how I get the ControllerFactory from the web.config

private IController CreateController(string controllerName)
{
    N2MVCConfigurationHandler config = ConfigurationManager.GetSection("Nsquared2/N2MVCSection") as N2MVCConfigurationHandler;

    IN2ControllerFactory controllerFactory = N2ControllerFactoryBuilder.GetControllerFactory(config.ControllerFactory);

    IController controller = controllerFactory.CreateController(base.RequestContext, controllerName);

    return controller;
}

I use some other helper classes in my code to create an instance of the Controller- and ViewFactory specified in the web.config, the code of the helper methods is not relevant for this post. When the public CTP of the MVC Framework is released, you can drop me an e-mail and I can send you my source code.

Here is the config section in the web.config where a conrollerFactory is specified and also a viewFactory. So we can now easy specify our ControllerFactory and ViewFactory in web.config.

<Nsquared2>
    <N2MVCSection
        controllerFactory="MvcApplication.Models.N2ControllerFactory, MvcApplication, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
        viewFactory="MvcApplication.Models.N2ViewFactory, MvcApplication, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
</Nsquared2>

By using the Route objects’ RouteHandler, we can easy change the RouteHandler the MVC Framework should use.

RouteTable.Routes.Add(new Route
{
    Url = "[controller]/[action]/[id]",
    Defaults = new { action = "Index", id = (string)null },
    RouteHandler = typeof(N2MVCRouteHandler)
});

In this post you have seen how we can create our own IRouteHandler, and also replace the ControllerFactory with our own.

[C#]提高软件开发效率三板斧之二----利用CMP模式

mikel阅读(894)

提高软件开发效率三板斧之二

利用CMP模式

上一章给大家总体介绍了一些提高软件开发效率的技术和技巧,在这里将给大家演示一个软件体系结构的应用,我们引用《ASP.NET电子商务高级编程》的一句话来解释“体系结构”这个词,因为演示的这个架构正是这本书中提到的。

“我们通常将体系结构看作一系列服务的集合,用来执行其他代码,这些服务应当提供有效的功能和特性来完成一些特定目标。”建立体系结构时必须牢记几个目标

1.      尽量创建可重用的代码

2.       加强设计模式和最佳实践

3.       提供通用功能

4.       提供开发基础

了解了设计体系结构的目标后,还应该明确体系结构应该提供的服务,在深入讨论之前建议大家先研究一下IbuySpy,Nile,Pet Shop等站点的体系结构,这些站点都非常好,也很正确。但这个架构要构建一个易于维护,升级,增强功能的架构。先列举一下需要架构提供的服务列表。

1.      数据服务

这里用到的时托管容器式持久性模型(CMP),它是一种设计模式,这种模式下业务对象不考虑数据从何处来,以及如何存放。业务对象可以是纯粹的业务对象,它们维护业务数据并根据业务规则判断这些数据是否正确,并且和数据库访问层没有丝毫关系。关于CMP的更多详细的信息,可以去买一本清华出版的《ASP.NET电子商务高级编程》以及相关网站资料,这里给出两个相关链接。

2.       监测服务

这里提供一套标准的服务来完成软件的监测任务。这里的监测是指记录,跟踪,错误处理,还有其它与跟踪系统正在做什么以及谁正在使用系统等相关的任务。这些服务包括跟踪变量,记录不同来源的错误和其它信息,以及使用自定义异常等

3.       安全服务

设计安全服务是为了提供足够的安全,不过这里用到的安全服务非常简单,仅仅实现了身份验证的功能,其实安全在开发过程中是一项不容忽视且相当艰巨的任务,包括防止脚本注入,数据库注入,实现数字签名,混淆代码以防止代码斜路以及web服务中的一些安全实现等。最近我买了一本《ASP.NET安全性高级编程》,那本书对在asp.net开发过程中如何实现安全性这个问题有很完整的阐述,建议大家可以买一本看看。

4.       配置服务

配置服务是用来控制应用程序所需要的配置选项的。这里的配置服务也是比较简单的,不过可以以后根据需求改进设计并加强体系结构中的这个部分。

因为篇幅有限,不可能对架构中的每个项目的每个类都逐一分析讨论,这里给出一个简单的类的功能列表,以方便你理解和使用这个架构,当然还有好多更吸引人和有用的技巧在里面,建议你下载源代码后慢慢研究一下,就这个架构我零零碎碎的看了3个多月才开始真正的使用它并把它用到实际开发中去,源代码可以去wrox网站去下载,也可以在本篇文章提供的地址去下载,前面说了,这个架构有足够的扩展性,你完全可以根据你的需要来写一个针对accessoracleSQLPersistenceContainer类,或者在实现安全服务的

1.       CMPServices项目

CMPServices项目

功能

CMPConfigurationHandler.cs

它负责从web.config文件的自定义配置部分加载元数据配置信息

ContainerMappingSet.cs

元数据配置的顶层是XML根结点,包含一个容器映射的列表,这个容器映射的集合保存在ContainerMappingSet类的一个实例中。它具有基于XML结点进行自身初始化的功能,以及用于调试的手动装载功能

ContainerMapping.cs

实现容器映射,对单个对象关系映射的抽象。它允许一个容器(如SQL持久性容器或XML容器等)获得对象实例的足够信息,该对象实例可用于执行对象关系映射,它可以让对象实例保持对底层数据源的持久性。一个容器映射最多包含4个命令映射(限于CRUD标准的4个命令)。

CommandMapping.cs

命令映射是CRUD动作的表现,可以被底层数据源执行,对于SQL容器,定义了4CommandMapping,分别实现Insert,Update,Select,Delete方法的支持.它还包含一个参数列表,并使属性总是显示命令的名称(可以映射到某个存储过程)和提供者提示,并允许在同一个容器和数据源中从不同的数据库得到不同的命令。

CommandParameter.cs

它包含的信息可以将类示例的公共属性和字段映射到数据源上使用的特定命令参数,它包括下列属性:类属性,参数名称,数据类型,大小,参数方向。

PersistableObject.cs

这是一个基类,它提供一系列让对象参与托管容器式持久性的基本功能,任何希望它的实例参与托管容器式持久性的类都必须继承这个类,它提供单条需要保持的数据。如果需要从数据库或者其它数据源获得多条数据查询结果,可以利用这个类。

PersistableObjectSet.cs

这个类继承自PersistableObject类如果不能提前返回数据集的大小,就需要用这个类来维护一个内部数据集,这个内部数据集相当于容器执行命令后的返回数据的存储处。容器可以事先知道它处理的是PersistableObject类还是PersistableObjectSet类可以动态决定命令执行后将返回的数据结果保存在哪里。

StdPersistenceContainer.cs

StdPersistenceContainer类是一个基类,它定义了所有容器必须提供的核心功能。这里采用类而不是接口,所以可以实现一些默认的功能,并且可以在整个架构中利用它的多态性使问题简化。

SqlPersistenceContainer.cs

SqlPersistenceContainer是持久性容器的一个实现,是专门为SQLServer设计的,可以针对SQLServer数据库的select,insert,update,delete操作方法。该类的其它内部容器映射来分析对象的关系映射,在对象实例和数据源之间交换数据。

2.       ConfigurationServices项目

ConfigurationServices项目

功能

SiteProfile.cs

在这里主要提供了一些程序运行的所有对象可以用到的静态属性,比如加载元数据,CMP数据引擎的数据库类型集合,指明默认数据源的连接字符串等。这些静态成员在应用程序启动时被加载起来,也就是在Global.asax文件里实现的,下面我会讲的。

 

3.       MonitorServices项目

MonitorServices项目

功能

GWTrace.cs

GWTrace.类负责提供标准的跟踪功能。该类统一了跟踪信息的存放位置。它知道web.config文件里的一个跟踪级别开关,但跟踪不同等级详细信息时会进行识别。合理应用该类的方法可以大大方便你在开发中对代码运行的跟踪。

MonitorUtilities.cs

该类提供了一些工具类方法,在监测系统中对其它方法非常有用,例如包含了返回机器名,应用程序名称,进程信息的方法它还提供了一个SafeFormat方法,它提供了一个安全,不引起错误的办法把对象参数转换成标准格式的字符串。

4.       MonitorServicesLogging项目

MonitorServicesLogging项目

功能

DbErrorEntry.cs

该类是一个PersistableObject类,在存储一条错误信息记录到数据库时,它用作与数据库的接口。它只是写入数据库或者读出的错误记录信息的一个简单的占位符号。

DbErrorEntrySet.cs

该类时一个PersistableObjectSet类,包含了在一定数据范围内返回一列错误所需的参数信息。为了管理而检查一个特定时间段的错误记录的时候,该类非常有用

DbErrorLog.cs

该类负责在数据库中维护错误记录信息,这些错误记录包含错误来源,运行时返回的进程等详细信息,以及实际的异常内容

FileErrorLog.cs

该类只是简单的把错误信息记录到一个文件,虽然该类可以单独使用,但开发者不应该单独调用它,而应该调用ErrorLog类。ErrorLog类会把错误先记录到数据库,只把文本文件作为一种数据库出问题时的备份

ErrorLog.cs

该类用于筛选错误记录,它首先尝试把错误记录送到数据库中。如果失败,他会把错误信息写入文件;如果成功,他会在文件中留下信息指出错误记录已经成功保存在数据库,并附带一个合适的错误ID

GWException.cs

这是一个自定义异常类,当实例化改类时,类实例可获得的错误信息就会发送给ErrorLog类,以将其持久保存到数据库或相应的文本文件中,这样就保证了开发者在任何时候实例化改类或其子类对应的异常时,错误信息都能够以可靠的形式保存下来。它还有个方法将基于一个数字键在内部资源文件ErrorMessages.resx中检索异常信息。这个数字键由GWExceptionMessages的枚举项提供

GWExceptionMessages.cs

改类时包含了证书值的枚举项,在加载错误信息字符串时,这些证书值在资源文件内被用做键。

架构已经有了,现在想办法把这个架构利用到实际的开发中,这里演示一个简单的留言板程序,其中也实现了对数据库的添加,删除,修改,选择等基本功能。其实一个大的项目也无非是由这样小的独立的功能模块构建起来的,这里只是抛砖引玉。闲话不说,我们来一步一步进行。

1.       构建项目

新建立一个web项目,这里是wawabook.然后把CMPServicesConfigurationServicesMonitorServicesMonitorServicesLogging4个文件夹拷贝到项目根目录下,并在解决方案里添加已有项目,把那这个项目添加进去。另外还有SharedAssemblyInfo.csGadgetsWarehouse.snk也拷贝到根目录下,一个是版权信息,一个是程序集的强名键。不用改动直接复制过来就可以。完成后解决方案管理器大致如下图。注意一下各个项目之间的依赖性,因为各个项目之间有交叉引用,关于各个项目之间的关系可以在源代码中看出来。

2.       建立数据库和相关表,存储过程

按实际的需求在SQLServer里建立数据库,表以及相应的存储过程。这里的数据库名叫wawabook,表名叫GuestBook,在创建表时最好把字段的描述信息写上,这时很有用的,方便以后查看和理解,完成后如下图。


分别建立对GuestBook表的Insert,Select,Update,Delete的存储过程如图

这里只给出insert_GuestBook的存储过程源码供参考,其它存储过程可以在本文附带的数据库源码中查看


Create PROCEDURE [insert_GuestBook]

(
@UserName     [nvarchar](10),

 
@Mail    [varchar](50),

 
@Title    [nvarchar](50),

 
@Content        [ntext],

 
@AddDate       [datetime],

 
@IPAddress     [varchar](50))

 

AS Insert INTO [wawabook].[dbo].[GuestBook] 

 ( 
[UserName],

 
[Mail],

 
[Title],

 
[Content],

 
[AddDate],

 
[IPAddress]

 

VALUES 

@UserName,

 
@Mail,

 
@Title,

 
@Content,

 
@AddDate,

 
@IPAddress)

GO

另外把SystemErrors表,GW_Insert_ERRORLOG_SP存储过程复制到你新建的数据库里,因为程序出错的时候会用到这个表和存储过程来记录错误日志。 你可以自己写一个GW_GET_ERRORLOG_SP存储过程来在出错误的时候查看错误日志以分析错误的原因所在。

3.       配置web.config

这里要配置几个地方,数据库连接字符串,跟踪级别开关,元数据等,这里只说一下额外需要特殊配置的地方,具体的web.config配置根据你的程序需要而定,比如说安全小节,身份验证小节,http小节等。

1)因为元数据在web.config文件保存,而web.config是个固定格式的文件,这就要指定这个自定义节点。

    <configSections>

         
<section name="GWConfig" type="GW.CMPServices.CMPConfigurationHandler, GW.CMPServices" />

</configSections>

2)为了让跟踪项目能够合理的工作,需要定义项目跟踪级别,下面的小节定义了项目的跟踪级别,具体可以根据你项目实际需求来设置。要了解TraceSwitch更多的消息请查阅MSDN文档

  <system.diagnostics>

         
<switches>

              
<add name="GWTrace" value="4" />

         
</switches>

</system.diagnostics>

需要注意的是上面两个小节都在system.web节点上面

       3)刚才定义了GwCfonfig自定义节点,这里用来放置CMP元数据,因为架构里的错误处理功能需要把错误信息保存到数据库里,所以错误处理所需要的元数据是必须的。另外这里给出了GuestBook容器的insert的元数据。你可以看一下元数据的结构,理解一下。因为很简单,相信你能很快写出相应的update,select,delete的元数据,记着,这些元数据和你刚刚写的存储过程是对应的。

<GWConfig>

         
<ContainerMappingSet>

              
<ContainerMapping>

                   
<ContainerMappingId>ERROR_LOG</ContainerMappingId>

                   
<ContainedClass>DbErrorEntry</ContainedClass>

                   
<Insert>

                       
<CommandName>GW_Insert_ERRORLOG_SP</CommandName>

                       
<Parameter>

                            
<ClassMember>ErrorMessage</ClassMember>

                            
<ParameterName>@ErrorMessage</ParameterName>

                            
<DbTypeHint>Varchar</DbTypeHint>

                            
<ParamDirection>Input</ParamDirection>

                            
<Size>4000</Size>

                       
</Parameter>

                       
<Parameter>

                            
<ClassMember>ExtendedInfo</ClassMember>

                            
<ParameterName>@ExtendedInfo</ParameterName>

                            
<DbTypeHint>Varchar</DbTypeHint>

                            
<ParamDirection>Input</ParamDirection>

                            
<Size>4000</Size>

                       
</Parameter>

                       
<Parameter>

                            
<ClassMember>ServerName</ClassMember>

                            
<ParameterName>@ServerName</ParameterName>

                            
<DbTypeHint>Varchar</DbTypeHint>

                            
<ParamDirection>Input</ParamDirection>

                            
<Size>50</Size>

                       
</Parameter>

                       
<Parameter>

                            
<ClassMember>ErrorId</ClassMember>

                            
<ParameterName>@ErrorId</ParameterName>

                            
<DbTypeHint>Int</DbTypeHint>

                            
<ParamDirection>Output</ParamDirection>

                            
<Size>4</Size>

                       
</Parameter>

                   
</Insert>

                   
<Select>

                       
<CommandName>GW_GET_ERRORLOG_SP</CommandName>

                       
<Parameter>

                            
<ClassMember>StartDate</ClassMember>

                            
<ParameterName>@StartDate</ParameterName>

                            
<DbTypeHint>Date</DbTypeHint>

                            
<ParamDirection>Input</ParamDirection>

                            
<Size>8</Size>

                       
</Parameter>

                       
<Parameter>

                            
<ClassMember>EndDate</ClassMember>

                            
<ParameterName>@EndDate</ParameterName>

                            
<DbTypeHint>Date</DbTypeHint>

                            
<ParamDirection>Input</ParamDirection>

                            
<Size>8</Size>

                       
</Parameter>

                   
</Select>

              
</ContainerMapping>

              
<ContainerMapping>

                   
<ContainerMappingId>GuestBook</ContainerMappingId>

                   
<ContainedClass>GuestBookSet</ContainedClass>

                   
<Insert>

                       
<CommandName>insert_GuestBook</CommandName>

                       
<Parameter>

                            
<ClassMember>UserName</ClassMember>

                            
<ParameterName>@UserName</ParameterName>

                            
<DbTypeHint>NVarChar</DbTypeHint>

                            
<ParamDirection>Input</ParamDirection>

                            
<Size>10</Size>

                       
</Parameter>

                       
<Parameter>

                            
<ClassMember>Mail</ClassMember>

                            
<ParameterName>@Mail</ParameterName>

                            
<DbTypeHint>Varchar</DbTypeHint>

                            
<ParamDirection>Input</ParamDirection>

                            
<Size>50</Size>

                       
</Parameter>

                       
<Parameter>

                            
<ClassMember>Title</ClassMember>

                            
<ParameterName>@Title</ParameterName>

                            
<DbTypeHint>NVarChar</DbTypeHint>

                            
<ParamDirection>Input</ParamDirection>

                            
<Size>50</Size>

                       
</Parameter>

                       
<Parameter>

                            
<ClassMember>Content</ClassMember>

                            
<ParameterName>@Content</ParameterName>

                            
<DbTypeHint>NText</DbTypeHint>

                            
<ParamDirection>Input</ParamDirection>

                            
<Size>10000</Size>

                       
</Parameter>

                       
<Parameter>

                            
<ClassMember>AddDate</ClassMember>

                            
<ParameterName>@AddDate</ParameterName>

                            
<DbTypeHint>Date</DbTypeHint>

                            
<ParamDirection>Input</ParamDirection>

                            
<Size>50</Size>

                       
</Parameter>

                       
<Parameter>

                            
<ClassMember>IPAddress</ClassMember>

                            
<ParameterName>@IPAddress</ParameterName>

                            
<DbTypeHint>Varchar</DbTypeHint>

                            
<ParamDirection>Input</ParamDirection>

                            
<Size>50</Size>

                       
</Parameter>

                   
</Insert>

              
</ContainerMapping>

         
</ContainerMappingSet>

     
</GWConfig>

4)其它固定配置

这里包括文本类型错误日志保存的目录和数据库链接字符串,这些东西一般保存在appSeettings小节里,值得注意的是为了安全考虑数据库链接字符串可以加密后再保存。Windows提供了一些API来增强安全性,.NET框架也提供了若干个加密相关的类,关于数据库连接字符串的加密可以参考一下PetShop3DataProtector类和ConnectionInfo类,这两个类都在Utility项目下。

<appSettings>

         
<add key="ErrorViewUrlPrefix" value="/GWSetup/ErrorLog/" />

         
<add key="ErrorLogBaseDir" value="E:\me\web.net\GWSetup\Errors\" />

         
<add key="DefaultDataSource" value="user id=sa; password=sa; database=wawabook; server=(local);" />

</appSettings>

当应用程序出错的时候错误处理程序会先试图在数据库做错误日志记录,如果记录成功,会在一个文本文件里做一个标志,这个文本文件的存放位置由ErrorViewUrlPrefix小节配置,如果在往数据库写错误日志也出错的话会以文本的形式记录错误日志,这个文本文件的存放位置由ErrorLogBaseDir小节来配置。

4.       修改Global.asax.cs里面的一些地方

这里要解决的问题是要在应用程序启动的时把CMP元数据加载起来,还有一些全局的变量也要在这里加载,比如说元数据映射的SQL数据类型。我们主要看一下Application_Start方法。

protected void Application_Start(Object sender, EventArgs e)

         
{

              System.Configuration.ConfigurationSettings.GetConfig(
"GWConfig");

 

              SiteProfile.DefaultDataSource 
= System.Configuration.ConfigurationSettings.AppSettings["DefaultDataSource"];

              

              SiteProfile.DbTypeHints[
"Varchar"= System.Data.SqlDbType.VarChar;

              SiteProfile.DbTypeHints[
"Int"= System.Data.SqlDbType.Int;

              SiteProfile.DbTypeHints[
"Date"= System.Data.SqlDbType.DateTime;

              SiteProfile.DbTypeHints[
"Text"= System.Data.SqlDbType.Text;

              SiteProfile.DbTypeHints[
"Bit"= System.Data.SqlDbType.Bit;

              SiteProfile.DbTypeHints[
"Money"= System.Data.SqlDbType.Money;

              SiteProfile.DbTypeHints[
"NVarChar"= System.Data.SqlDbType.NVarChar;

              SiteProfile.DbTypeHints[
"NText"= System.Data.SqlDbType.NText;

     }


如果你的程序里需要加载其它的sql类型,你可以对这个方法做适当的修改。

5.       建立BLL层和数据实体类

为了简单,我把业务逻辑层和数据库实体层放在了一个项目里。建立GuestBookBLL项目,并在里面添加两个类GuestBookGuestBookSet.GuestBookSet类时数据实体类,必须继承自PersistableObject类或者PersistableObjectSet.类,因为这里要用到一些大数据集,所以继承自PersistableObjectSet.

using System;

using System.Data;

using System.Xml;

 

using GW.CMPServices;

 

namespace GuestBookBLL

{

     
/// <summary>

     
/// GuestBookSet 的摘要说明。

     
/// </summary>


     
public class GuestBookSet:PersistableObjectSet

     
{

         
private int _id ;

         
private string _UserName;

         
private string _Mail;

         
private string _Title;

         
private string _Content;

         
private DateTime _AddDate;

         
private string _IPAddress;

         
public GuestBookSet()

         
{

              
//

              
// TODO: 在此处添加构造函数逻辑

              
//

         }


         
public int id

         
{

              
get 

              
{

                   
return this._id;

              }


              
set 

              
{

                   
this._id = value;

              }


         }


         
public string UserName

         
{

              
get 

              
{

                   
return this._UserName;

              }


              
set 

              
{

                   
this._UserName = value;

              }


         }


 

         
public string Mail

         
{

              
get 

              
{

                   
return this._Mail;

              }


              
set 

              
{

                   
this._Mail = value;

              }


         }


         
public string Title

         
{

              
get 

              
{

                   
return this._Title;

              }


              
set 

              
{

                   
this._Title = value;

              }


         }


         
public string Content

         
{

              
get 

              
{

                  
return this._Content;

              }


              
set 

              
{

                   
this._Content = value;

              }


         }


         
public DateTime AddDate

         
{

              
get 

              
{

                   
return this._AddDate;

              }


              
set 

              
{

                   
this._AddDate = value;

              }


         }


         
public string IPAddress

         
{

              
get 

              
{

                   
return this._IPAddress;

              }


              
set 

              
{

                   
this._IPAddress = value;

              }


         }


     }


}


可以看出这个类和刚才我们建立的数据库是对应的。需要注意的是你在定义类的属性时要和数据库的类型相对应,比如说数据库的字段时VarChar类型,对应的C#类型就是string类型,如果字段类型是DateTime类型,对应的C#类型是System.DateTime结构类型。关于更多的类型对应关系在MSDN里可以查到。

有了数据库实体类,我们就可以写业务逻辑了,业务逻辑我们在GuestBook类里实现,这里先看一下怎样利用架构来向数据库里写入数据,也就是填写留言功能。

public static void insert_GuestBook( string UserName,string Mail,string Title,string Content,DateTime AddDate,string IPAddress )

         
{

              GWTrace.EnteringMethod( MethodBase.GetCurrentMethod() );

              SqlPersistenceContainer spc 
= new SqlPersistenceContainer(CMPConfigurationHandler.ContainerMaps["GuestBook"]);

              GuestBookSet gbs 
= new GuestBookSet();

              gbs.UserName
=UserName;

              gbs.Mail
=Mail;

              gbs.Title
=Title;

              gbs.Content
=Content;

              gbs.AddDate
=AddDate;

              gbs.IPAddress
=IPAddress;

              spc.Insert( gbs );

         }


可以看到,先实例化一个SqlPersistenceContainer容器类,然后实例化一个GuestBookSet类,并设置这个类相应的属性,最后运行sql容器类的Insert()方法就可以完成数据插入功能了,因为SqlPersistenceContainer类已经继承了跟踪机制,所以直接在调用这个方法的时候如果.aspx页面启用了Trace就可以看到每个参数详细的赋值过程,为你跟踪应用程序提供了非常有价值的信息,如果出错你会看到程序停止在哪一步,或者能看到是由于哪个参数导致了程序运行错误。这个方法里还用到了GWTrace.EnteringMethod方法,这个方法是跟踪本方法的运行情况的,如果你想跟踪某个方法的运行情况就可以直接用那个方法,前提是你引入了以下名字控件

using GW.CMPServices;

using GW.MonitorServices;

using System.Reflection;

using System.Diagnostics;

using GW.MonitorServices.Logging;

我们来看一下前台页面在调用这个方法是trace里显示的信息。

看到了吗,是不是和我说的一样,根据这里显示的信息你可以知道到底是哪里出了错误,比如说是存储过程还是页面传入的非法参数。

我们再看一下Select方法的实现,先看存储过程。

Create PROCEDURE [select_GuestBook]

         (
@id          [int])

          
AS 

if @id=-1 

         
select * from [wawabook].[dbo].[GuestBook] 

else

         
select * from [wawabook].[dbo].[GuestBook] Where ( [id]     = @id       )

GO

再来看元数据

<Select>

     
<CommandName>select_GuestBook</CommandName>

     
<Parameter>

         
<ClassMember>id</ClassMember>

         
<ParameterName>@id</ParameterName>

         
<DbTypeHint>Int</DbTypeHint>

         
<ParamDirection>Input</ParamDirection>

         
<Size>4</Size>

     
</Parameter>

</Select>

再来看业务逻辑层对应代码

        public static DataSet select_GuestBook(int id)

         
{

              GWTrace.EnteringMethod( MethodBase.GetCurrentMethod() );

              SqlPersistenceContainer spc 
= new SqlPersistenceContainer(CMPConfigurationHandler.ContainerMaps["GuestBook"]);

              GuestBookSet gbs 
= new GuestBookSet();

              gbs.id
=id;

              spc.Select( gbs );

              
return gbs.ResultSet;

     }

最后看一下页面的编码类中相应的代码

private void bind_dgrd1()

     
{

         
//获取所有记录用来作为DataGrid的数据源

         dgrd1.DataSource
=GuestBook.select_GuestBook(1);

         dgrd1.DataBind();

     }


     
private void dgrd1_SelectedIndexChanged(object sender, System.EventArgs e)

     
{

         
//当选择DataGrid上的一列是选择这列数据绑定到Label的Text属性上

         
int id=(int)dgrd1.DataKeys[dgrd1.SelectedIndex];

         System.Text.StringBuilder sb
=new System.Text.StringBuilder();

         DataRow dr
=GuestBook.select_GuestBook(id).Tables[0].Rows[0];

         sb.AppendFormat(
"Title:{0}<br>Content:{1}",dr["Title"].ToString(),dr["Content"].ToString());

         lbl1.Text
=sb.ToString();

}


流程已经很明显了,你每添加一条业务规则,都要建立数据表,写存储过程,配置元数据,写业务逻辑代码,在aspx.cs调用逻辑层代码。你可以试着把留言的修改和删除功能也做出来,也就是试着使用SqlPersistenceContainer类的Update方法和Delete方法,记住操作流程了吗?先实例一个SqlPersistenceContainer类,接着实例化一个业务实体类,设置业务实体类属性,最后执行SqlPersistenceContainer类的某个方法,错误处理已经继承了,可以方便你查看出错的地方,另外你还可以抛出自定义异常GWException来自动记录应用程序正式运行后所发生的错误日志。

       最后建议大家再下载架构源代码后仔细研究一下,这个 架构在你开发的整个周期都在起作用,无论是前期的调试测试还是后期的编码部署和运行,利用好它会给你带来很多的好处,我这里没有引入实现安全的项目,因为 那几个类非常简单,也不具有相当的通用性,你可以利用现有的架构来构建一个适合你自己的安全的类库,另外你也可以继续扩展你的架构中可以重复利用的功能, 比如说建立一个tools类库,把上传文件,发送邮件,过滤危险字符等通用功能集成到里面,以便其它人员随时调用,而不是再从新写一个,一般服务性的类多做一些静态方法以便不用实例化就可以调用。其实你可能已经发现了,存储过程,CMP元数据和数据实体类以及业务逻辑类大多都是和数据库的某个表相对应的,我们每次都需要一边看着数据库的字段一边来构建自己的代码,如果字段非常的多,将会是一个非常繁琐的工作,下次我会和大家一起DIY一个代码生成器,帮助你生成一部分相关代码,免除你一些重复工作。

asp.net电子商务高级编程》源码下载如下

http://support.apress.com/books.asp?bID=1861008031&s=0&Go=Select+Book

[C#]浅谈.Net版(C#)的CMP模式 - 一个上线商城的系统架构

mikel阅读(897)

商城上线快2、3个月了,一直都懒得写点东西,在加上杂七杂八的事情也比较忙,所以都没有把这个系统当时做的整个架构思绪整理清,昨天才从深圳完了两天回来,怎感觉是要做的事来着.刚开始接触CMP模式的时候也是看了它几天,到谷歌百度里面一搜,我们博客园里面就有蛙蛙池塘提高软件开发效率三板斧之二利用CMP模式一文里有它的详细介绍,在这里我自己也对这个用CMP模式拿来真正上项目时候的问题来做个总结.

  • 项目名称:惠海IT商城
  • 网        址:http://http://www.huihaimall.com/
  • 开发环境:WinXP SP3、IIS5.0、Dreamweaver、VS 2005、SQL-Server 2000
  • 项目描述:项目实现了商品的浏览筛选(主要是公司做的IT产品)、会员商品收藏、订购(订单)、发邮件推荐给朋友、会员积分、收货地址薄、DIY自主装机等,业务逻辑全部在本项目中用.NET(CMP)实现,而展示就不一定都是用.net的aspx页面来做,如DIY装机就是用Flex生成flash来实现的,但是它们都是同步的(同登陆同注销,包括会员产品收藏等).还有一个最重要的就是后台管理也是用Flex调用.net来实现的,由于要提供Flex调用的接口,所有还提供了几个WebService的页面(关与身份验证请见:在WebService中使用Session或Cookie—实现WebService身份验证(客户端是Flex) ),另外在用JQuery发送Ajax请求的时候页面传输数据时候还有用到Json数据(Flex好象有几个地方也用到了).
  • 项目解决方案截图如下:

    下面,我对上图所示以我的了解进行简要的介绍:
  1. CMPServices 它里面主要是一些CMP配置和服务的基类,他们对应的名称和功能内如下所:
    名称 功能描述  
    CMPConfigurationHandler 继承自IConfigurationSectionHandler,用来读取在Web.Config文件内的自定义CMP配置.
    CommandMapping 命令映射类,用于某一个业务的容器,一般为对应Insert、Delete、Update、Select里某一个存储过程名为CommandName,里面可能包含多个CommandParameter.  
    CommandParameter 存 储过程参数类,里面有ParameterName、Size、DbTypeHint等属性,还有一个ClassMember的属性,表示对应实体模型的属 性,一般ParameterName为@Name而ClassMember值就为Name(预先配置好的),因为一般存储过程的参数名就对应数据库实体模 型的列字段.  
    ContainerMapping 容 器映射类,一般为一个业务实体,比如用户,它里面就有Insert、Delete、Update、Select这4个CommandMapping,而且 它有个key在CMP里面的映射ID叫ContainerMappingId和ContainedClass对应为实体对象模型名.  
    ContainerMappingSet 多个ContainerMapping容器映射集合类,里面的Hashtable可根据ContainerMappingId映射ID的key来匹配ContainerMapping.  
    PersistableObject 持久对象基类,实体类继承它能实现对数据的保存(一般为Insert、Delete、Update命令操作).  
    PersistableObjectSet 继承自PersistableObject,实现数据持久化保存结果(一般为Select命令操作).  
    SQLPersistenceContainer 业务的容器基类,构造函数需ContainerMapping,包含Insert、Delete、Update、Select四个虚方法.  
    StdPersistenceContainer 业务的容器,构造函数ContainerMapping调用父类构造函数,根据ContainerMapping对Insert、Delete、Update、Select四个方法进行具体的实现.  

  2. ConfigurationServices 里面就一个类SiteProfile,它里面主要是一些静态成员,如有DefaultDataSource是数据源连接字符串,还有一个 Hashtable的DbTypeHints,作用是用来配置一些存储过程的参数对应的ADO.Net里面的SQL数据类型.
  3. http://localhost/WebSite/ 这层就不用说了,就是我们的网页表示层.
  4. MallMemberDAL 这个是商城里面的一个模块,为商城会员业务逻辑层分开为一个项目,里面主要为会员的一些操作,是为CMP的具体应用了,类的命名如下图所示:

    上图是对会员收货地址和优惠卷的实例,其中Item结尾的继承自PersistableObject的实体类,Manager为所有操作方法的集合类(以 静态方式提供),Set继承自PersistableObjectSet为数据集合的容器类,其实最初CMP里实体的命名不是这样的,好象是加 Entity后缀,这个就看你怎么决定了,但是整个项目一定要统一.
  5. MonitorServices 为CMP的监控服务项目,主要是跟踪当前执行的方法、异常信息等,这里就不祥说了.
  6. MonitorServicesLogging 监控服务日志项目,用来处理异常错误信息,可以保存到数据库内,如果保存到数据库失败则写到本地日志文件里,不过我在项目里面并没有用到它的,一刚开始因 为还没有整个摸透它,好象还要在Web.Config文件内设置GWTrace的跟踪等级,刚开始做的时候老是报错,但是没有把错误写到日志里面,而是 CMP老是抛出那同一个错误,感觉很是麻烦搞的满头包就没去理它了,直到后来发布网站上线的时候也把这个错误日志的功能忘记加了.
  7. Newtonsoft.Json 它是一个完全开源可以免费使用的数据格式,应用领域.Net、JavaScript、Flex至少我知道有这么多,它能一个.Net实体对象通过它的格式 化字符串传输到另一端又能转换成原对象,比如我能在使用了Json的JavaScript里面使用实体名.属性设置或获取值再在.Net里能轻易得到,同 理Flex里面也有Json的对象格式,它使得我们能使用字符串轻易的传输实体对象,在本项目里面就有使用Flex通过Json数据与.Net通 讯,JavaScript使用Ajax来Post传递Json数据.虽然它还存在一些的Bug,不过基本上所有的软件都会存在一些Bug的,在本项目应用 中,好象Json就有一个数据类型转换的Bug,不过还好,Json是开源的,直接在它的源代码上修改解决了这个Bug. 
  8. 最后还有2个模块:PopedomDAL和ProductDAL分别是权限和产品模块,实质是和MallMemberDAL差不多,也就不多说了.
  • 下面来说一下Web.Config文件的相关配置
  • 在configuration下的configSections的第一个子节点,配置CMP读取的自定义节点
    <section name="GWConfig" type="Huihai.Mall.CMPServices.CMPConfigurationHandler, Huihai.Mall.CMPServices" />
  • 再在configSections下增加CMP跟踪监视等级及数据库连接字符串,默认本地日志文件路径等配置(以下为我项目里面的一些CMP配置,最下面的为商城的一些配置)
    <system.diagnostics>
    <switches>
    <add name="GWTrace" value="4" />
    </switches>
    </system.diagnostics>
    <appSettings>
    <add key="ErrorViewUrlPrefix" value="/Wawacrm/ErrorLog/" />
    <add key="ErrorLogBaseDir" value="E:\me\bak\oa\WAWACRM\Errors\" />
    <add key="DefaultDataSource" value="server=127.0.0.1;database=HH_System;uid=sa;pwd=123" />
    <add key="picurl" value="Upload/product/" />
    <add key="score" value="1" />
    <add key="isShowRunTime" value="true" />
    <!– 产品评论 –>
    <add key="CommentIsAudit" value="Y" />
    <!– 产品评论回复 –>
    <add key="ReCommentIsAudit" value="Y" />
    <!– 产品咨询 –>
    <add key="ConsultIsAudit" value="N" />
    <!– 商城咨询反馈 –>
    <add key="FeedbackIsAudit" value="Y" />
    <!– 后台管理员的使用的邮箱名后缀 –>
    <add key="EmailPostfix" value="@coreoa.cn" />
    </appSettings>
  • 接 下来最重要的也是最复杂的就是GWConfig自定义CMP元数据配置的节点了, 它里面主要是配置每一个存储过程,对应容易实体类,及该业务实体的Insert、Update、Delete、Select四个方法的参数的详细描述,已 上面MallMemberDAL项目的Address和Coupon为例,它的配置为如下:
    <GWConfig>
    <ContainerMappingSet>
    <ContainerMapping>
    <ContainerMappingId>Address</ContainerMappingId>
    <ContainedClass>AddressItem</ContainedClass>
    <Insert>
    <CommandName>MO_Address_Insert</CommandName>
    <Parameter>
    <ClassMember>Member_inner_code</ClassMember>
    <ParameterName>@member_inner_code</ParameterName>
    <DbTypeHint>Int</DbTypeHint>
    <ParamDirection>Input</ParamDirection>
    <Size>4</Size>
    </Parameter>
    <Parameter>
    <ClassMember>Province</ClassMember>
    <ParameterName>@province</ParameterName>
    <DbTypeHint>Varchar</DbTypeHint>
    <ParamDirection>Input</ParamDirection>
    <Size>20</Size>
    </Parameter>
    <Parameter>
    <ClassMember>City</ClassMember>
    <ParameterName>@city</ParameterName>
    <DbTypeHint>Varchar</DbTypeHint>
    <ParamDirection>Input</ParamDirection>
    <Size>20</Size>
    </Parameter>
    <Parameter>
    <ClassMember>County</ClassMember>
    <ParameterName>@county</ParameterName>
    <DbTypeHint>Varchar</DbTypeHint>
    <ParamDirection>Input</ParamDirection>
    <Size>20</Size>
    </Parameter>
    <Parameter>
    <ClassMember>Zip</ClassMember>
    <ParameterName>@zip</ParameterName>
    <DbTypeHint>Varchar</DbTypeHint>
    <ParamDirection>Input</ParamDirection>
    <Size>6</Size>
    </Parameter>
    <Parameter>
    <ClassMember>Address</ClassMember>
    <ParameterName>@address</ParameterName>
    <DbTypeHint>Varchar</DbTypeHint>
    <ParamDirection>Input</ParamDirection>
    <Size>100</Size>
    </Parameter>
    <Parameter>
    <ClassMember>Name</ClassMember>
    <ParameterName>@name</ParameterName>
    <DbTypeHint>Varchar</DbTypeHint>
    <ParamDirection>Input</ParamDirection>
    <Size>50</Size>
    </Parameter>
    <Parameter>
    <ClassMember>Mobile</ClassMember>
    <ParameterName>@mobile</ParameterName>
    <DbTypeHint>Varchar</DbTypeHint>
    <ParamDirection>Input</ParamDirection>
    <Size>15</Size>
    </Parameter>
    <Parameter>
    <ClassMember>Tel</ClassMember>
    <ParameterName>@tel</ParameterName>
    <DbTypeHint>Varchar</DbTypeHint>
    <ParamDirection>Input</ParamDirection>
    <Size>50</Size>
    </Parameter>
    <Parameter>
    <ClassMember>IsDefault</ClassMember>
    <ParameterName>@isDefault</ParameterName>
    <DbTypeHint>Char</DbTypeHint>
    <ParamDirection>Input</ParamDirection>
    <Size>1</Size>
    </Parameter>
    </Insert>
    </ContainerMapping>
    <ContainerMapping>
    <ContainerMappingId>Coupon</ContainerMappingId>
    <ContainedClass>CouponItem</ContainedClass>
    <Select>
    <CommandName>MO_Coupon_Select</CommandName>
    <Parameter>
    <ClassMember>Sn</ClassMember>
    <ParameterName>@sn</ParameterName>
    <DbTypeHint>Varchar</DbTypeHint>
    <ParamDirection>Input</ParamDirection>
    <Size>10</Size>
    </Parameter>
    <Parameter>
    <ClassMember>Password</ClassMember>
    <ParameterName>@password</ParameterName>
    <DbTypeHint>Varchar</DbTypeHint>
    <ParamDirection>Input</ParamDirection>
    <Size>10</Size>
    </Parameter>
    </Select>
    </ContainerMapping>
    </ContainerMappingSet>
    </GWConfig>
  • 以 上配置中,为简便示例Address只有配置Insert的方法,它调用名为MO_Address_Insert的存储过程,Insert节点下第一个子 节点CommandName即为存储过程名,而同级别的还有Parameter节点,每一个都代表存储过程的一个参数,下面有ClassMember、 ParamDirection等子节点即是对它的描述,其他Update、Delete、Select依此类推也可以按这种格式来配置;而Coupon只 有配置一个Select的节点(其他的也能按照上述增加配置,没有配置的方法将不能调用),Coupon是为ContainerMappingId容器业 务实体唯一Key,它对应名为CouponItem的业务实体模型类,而参数下面的ClassMember为实体下的成员名,一般就是实体类的属性,因为 它将根据这个名字反射获得实体的属性值.以上这么多配置但是却只实现了2个业务的方法,是不是有点太麻烦了?没得办法,最开始的CMP模式就是这样一个个 把存储过程配置好的!但是你的项目里面要用到的存储过程肯定不只与2个,姑且就不说CMP模式这样配置也只能用存储过程的,但是存储过程一多了就会很麻烦 了,每一个存储过程配置都要手写添家进去!要是存储过程上十来个参数又有增删改查的方法岂不要配置很多东西?的确要配置的东西不少,但是我们可以自己写的 小程序对CMP的配置执行操作,而不需要我们去打开Web.Config文件一个个去手改,下图为我自己写的一个对CMP的配置进行操作的截图:
  • 上 面主要是通过XPath表达试对Web.Config进行筛选读取CMP的配置节点并在GridView里面显示出来,并能直接对其进行修改等操作.如果 假如我们要增加一个的时候,我们可以点浏览用sysobjects把数据库里面所有的存储过程名字等信息读出来,选择要配置的那个存储过程后再用 sp_sproc_columns把它所有的参数信息读出来,如ParameterName、Size、ParamDirection等,注意 ClassMember(实体模型的属性名)读出来默认是ParameterName,因为在我的项目里面ParameterName名就跟实体的属性名 一样,你也可以改成其他的,但是你要根据你实体命名的约定好,因为它要用反射实体里找这个属性,如果找不到就不好了.
  • 是 不是用上面那个小工具爽多了?你不需要打开Web.Config手写一个配置,在数据库里面写一个存储过程就到这里配置一下直接在增加一个CMP的配置节 点到Web.Config文件内.,我这个商城的项目下来把所有业务的存储过程配置完Web.Config文件总大小有140KB,把 Web.Config文件撑得这么大对性能会不会有影响呢?我想应该不会有多大影响的,从CMP框架的运行来看,它只是在 Application_Start的时候读取一次配置文件到内存,以后就像读AppSettings一样直接在内存里面取,很方便 的.Application_Start运行的代码如下:
  • // 在应用程序启动时运行
    System.Configuration.ConfigurationManager.GetSection("GWConfig");
    SiteProfile.DefaultDataSource
    = System.Configuration.ConfigurationManager.AppSettings["DefaultDataSource"];
    SiteProfile.DbTypeHints[
    "Varchar"] = System.Data.SqlDbType.VarChar;
    SiteProfile.DbTypeHints[
    "Nvarchar"] = System.Data.SqlDbType.NVarChar;
    SiteProfile.DbTypeHints[
    "Int"] = System.Data.SqlDbType.Int;
    SiteProfile.DbTypeHints[
    "Date"] = System.Data.SqlDbType.DateTime;
    SiteProfile.DbTypeHints[
    "Text"] = System.Data.SqlDbType.Text;
    SiteProfile.DbTypeHints[
    "Bit"] = System.Data.SqlDbType.Bit;
    SiteProfile.DbTypeHints[
    "Money"] = System.Data.SqlDbType.Money;
    SiteProfile.DbTypeHints[
    "Datetime"] = System.Data.SqlDbType.DateTime;
    //后新添加2种对应类型
    SiteProfile.DbTypeHints["Char"] = System.Data.SqlDbType.Char;
    SiteProfile.DbTypeHints[
    "Numeric"] = System.Data.SqlDbType.Decimal;
    SiteProfile.DbTypeHints[
    "Smalldatetime"] = System.Data.SqlDbType.SmallDateTime;
    //监听配置文件的改变
    System.IO.FileSystemWatcher fsw = new System.IO.FileSystemWatcher(Server.MapPath("~/Upload/special"));
    fsw.Filter
    = "XMLFile.xml";
    fsw.NotifyFilter
    = System.IO.NotifyFilters.LastWrite;
    fsw.Changed
    += new System.IO.FileSystemEventHandler(ReadAdConfig);
    fsw.EnableRaisingEvents
    = true;
    ReadAdConfig(
    null, null);
  • 上 面代码主要是在程序启动的时候Web.Config的CMP配置的元数据读到CMP的一个静态类里面,初始化数据连接,设置存储过程参数对应的SQL数据 类型,后面监听和ReadAdConfig方法是我项目另外的东西,这里就不叉开话题说了. 配置搞清楚了,下面我们就来研究一下CMP这个架构到底是如何运行的呢? 还是以上面的那个例子为准,比如我要在Address里增加一个会员收货地址.我就在AddressManager里面提供一个 AddressInsert的静态方法供调用.
  • public static void AddressInsert(AddressItem item)
    {
    SqlPersistenceContainer spc
    = new SqlPersistenceContainer(CMPConfigurationHandler.ContainerMaps["Address"]);
    spc.Insert(item);
    }
  • 上面所示为CMP一个调用业务的过程,它实现的全过程大概为:
  • ①第一步: 在Application_Start把CMP的所有配置读取到CMPConfigurationHandler下面的ContainerMaps集合里面,它是一个Hashtable对象
  • ②第二步: 根据Address这个ContainerMappingId的key在CMPConfigurationHandler.ContainerMaps匹配到Address这个业务对象的ContainerMapping映射容器对象
  • ③第三步: 实例化SqlPersistenceContainer托管容器对象,ContainerMapping作为构造函数穿入,并使用: base(initCurrentMap)调用StdPersistenceContainer父类构造函数
  • ④第四步: 执行具体方法(这里为Insert操作),而参数为继承了PersistableObject类型的实体模型对象,为了更好的说明我也把Insert方法的代码给帖出来
  • /// <summary>
    ///
    /// </summary>
    /// <param name="insertObject"></param>
    public override void Insert(PersistableObject insertObject)
    {
    GWTrace.EnteringMethod(MethodBase.GetCurrentMethod());
    try
    {
    CommandMapping cmdMap
    = currentMap.InsertCommand;
    SqlCommand insertCommand
    = BuildCommandFromMapping(cmdMap);
    AssignValuesToParameters(cmdMap,
    ref insertCommand, insertObject);
    insertCommand.Connection.Open();
    insertCommand.ExecuteNonQuery();
    insertCommand.Connection.Close();
    AssignOutputValuesToInstance(cmdMap, insertCommand,
    ref insertObject);
    insertCommand.Dispose();
    }
    catch (Exception dbException)
    {
    string s = insertObject.ToXmlString();
    s
    +=currentMap.InsertCommand.CommandName;
    throw new Exception("Persistance (Insert) Failed for PersistableObject", dbException);
    }
    }
  • 由 于受空间限制没能把全部代码贴出来比较难看一点,大概原理是根据PersistableObject 对象(实际为保存在XML内的CMP的配置映射到的容器)和要执行的方法创建SqlCommand对象,设置它要执行的存储过程名称.再循环 Parameter创建并添加参数,再在PersistableObject 根据反射ClassMember获得实体里面的属性值设置Parameter参数值.然后在执行存储过程,并跟踪记录当前错误方法,处理异常信息,这样便 完成了CMP一个业务处理的全过程.
  • 是不是感觉这样调用有点'妙' 呢?你要修改那个业务对象你只需要指定对应的实体模型的属性值它就能作为参数Insert(Update或Delete)数据库表里面对应的列,如果存储 过程参数对应的实体属性值没有指定的话将传递的为默认值,如String类型的将为NULL.这里在反射的时候还要注意一个问题,就是列为时间类型的时 候,C#里面的DataTime为空的时候为初始值为0001-1-1 0:00:00,而当你使用反射的时候把这个时间更新到数据库会报错,提示什么数据溢出,因为SQL里时间类型是有一个时间段,所以在CMP调用 AssignValuesToParameters方法使用发射赋值的时候要加上一个判断,这样修改时间类型字段指定为空就没有问题了.
  • object o = PropertyInfo.GetValue(persistObject, null);
    if (o == null)
    o
    = DBNull.Value;
    if (o.GetType().Equals(typeof(System.DateTime)))
    {
    //时间默认值(即未给时间赋值),则为空
    if (o.ToString() == "0001-1-1 0:00:00")
    o
    = DBNull.Value;
    }
  • 而 Select查询操作略有一点不同, 因为它要返回结果解,这个时候就应该要用AddressSet类了,所有要实现查询操作的类都必须继承自PersistableObjectSet,因为 它能有返回DataSet数据集的实现,可以把它看成一个特殊的PersistableObject对象,因为它除开有Insert、Delete、 Update还有Select.AddressSet类其实也是一个实体类,但它跟AddressItem类不同的是它只管查询,而查询的存储过程往往没 有增、删、改的那么多的参数,因此它的里面只需要几个查询条件字段的属性.调用起来跟增、删、改的操作都差不多,也是根据反射它里面的属性值在赋个对应的 参数在执行,CMP默认返回的一个DataSet,我的觉得既然用到的实体,为什么不用它来代替DataSet呢?所以我在原来的基础上新加一个用反射把 DataSet转换成实体的数组,方便再次操作,代码如下:
  • /// <summary>
    /// 根据引用传来的Object实体对象使用反射给它的属性赋值
    /// </summary>
    /// <param name="obj">实体对象</param>
    /// <returns>是否给表里面的记录值填充实体里属性成功,如何找不到实体属性或记录集为空返回false</returns>
    public bool ResultSingleObject(ref Object obj)
    {
    //internalData是本类里返回的DataSet集合
    if (internalData.Tables.Count == 0)
    return false;
    DataTable tab
    = internalData.Tables[0];
    if (tab.Rows.Count == 0)
    return false;
    Type type
    = obj.GetType();
    foreach (DataColumn column in tab.Columns)
    {
    string columnName = column.ColumnName;
    object t = tab.Rows[0][columnName];
    PropertyInfo property
    = type.GetProperty(columnName);
    if (property == null)
    property
    = type.GetProperty(columnName.Substring(0, 1).ToUpper() + columnName.Substring(1, columnName.Length 1));
    if (property == null)
    property
    = type.GetProperty(columnName.ToUpper());
    if (property != null)
    {
    if (t.GetType().FullName != "System.DBNull")//如果数据库返回的值不等于NULL的情况下才给找到了实体的字段属性赋值
    property.SetValue(obj, t, null);
    }
    else
    throw new Exception("表的列名为" + columnName + "不能与实体名为" + type.Name + "的属性名一致,请修改过程的返回的列名称或实体属性名");//便与调试
    }
    return true;
    }
  • 通过如上代码for循环一下就能把DataSet转换成AddressItem数组或是List<AddressItem>泛型.
  • CMP模式差不多就这么多些吧,总结其中一些美中不足的地方:
  • Insert、 Delete、Update、Select这个四个方法还不能满足需求,增删改查这个四个方法是CMP的核心,但一个业务实体的操作只有这个4个方法往往 是不够的.因为这四个方法只能分别对应一个储存过程,而查询的存储过程一般一个难得搞定,比如用户表我除开根据用户ID去查询,还要根据用户名和密码,还 有可能要根据用户类型返回用户列表等等,实际的需求是复杂的.用CMP的话那我还需要另外单独配置2个业务实体来分别放根据用户名和密码和根据用户类型的 操作的存储过程,而它们里面的Select都只有一个且对应的实体也是同一个,是不是感觉有点浪费?感觉Insert、Delete、Update、 Select这个四个方法不够用,后来想到增加一个List的方法,基本上每一个表都有一个根据ID去查询记录的时候,Select就对应这个操作,而新 加的List就对应根据其他条件可能会返回多条记录的操作,这样就不需要当一个业务有2个查询操作的时候而再去新建一个了,但是新增一个List查询的方 法Web.Config内CMP配置文件也要加List节点的配置,而且CMP的基类里面也要增加一个List方法并要解析对应的List配置节点,由于 当时项目做得快差不多了,这人一懒呢就没有去完成了.-_-!!!
  • 连 续写了几个晚上终于快要完了,之所以我要写这些,是感觉自己最近好象都没做什么东西一样,白天在慢悠悠的上一天班,晚上就什么也不想动了,一坐到电脑前就 是看玩传奇世界或是看电视连续剧斗牛要不要、篮球火等啊,呵呵,等来得急看时间的时候已经凌晨过后了…第二天起来到公司上班,晚上又继续,我心是想我 不要每天就这样过去了,但是这样持续了好久一段时间…我有时候也自责自己到最后还是一事无成!既然来到了这行,就一定要做好这个职业的本职工作,IT 行业这个技术每天都在不断更新演变的领域你每天不去学习怎么能行呢?所以我不要再那么在'堕落'下去了,呵呵^_^,便决定写点东西,至少别自己做过的项 目都不记得去了.
  • 源代码就不要问了,放在公司的SVN服务器里面了,这里提供一个我原来的参考的CMP架构的源代码下载, 好象还是蛙蛙池塘2005年写的.
  • CMP模式参考源代码下载

[MVC]Improve scalability in ASP.NET MVC using Asyn

mikel阅读(1189)

ASP.NET applications run with a fixed-size thread pool. By default, they have 200 (or 250? I forget…) threads available to handle requests. You can make the thread pool bigger if you want, but it’s not usually helpful: more contention and overhead from thread switching will eventually actually reduce the server’s throughput. But what if most of your threads are actually sitting around doing nothing, because they’re actually waiting for some external I/O operation to complete, such as a database query or a call to an external web service? Surely things could be more efficient…

Well yes, actually. Ever since Windows NT 4 we’ve had a notion of I/O Completion Ports (IOCP) which are a mechanism for waiting for I/O to complete without causing thread contention in the meantime. .NET has a special thread pool reserved for threads waiting on IOCP, and you can take advantage of that in your ASP.NET MVC application.

image

The IHttpAsyncHandler, first introduced in ASP.NET 2.0, splits request processing into two. Instead of handling an entire request in one thread (expensive I/O and all), it first does some processing in the normal ASP.NET thread pool, as per any normal request, then when it’s time for I/O, it transfers control to the I/O thread, releasing the original ASP.NET thread to get on with other requests. When the I/O signals completion (via IOCP), ASP.NET claims another, possibly different thread from its worker pool to finish off the request. Thus, the ASP.NET thread pool doesn’t get ‘clogged up’ with threads that are actually just waiting for I/O.

Adding asynchronous processing to ASP.NET MVC

The MVC framework doesn’t (yet) come with any built-in support for asynchronous requests. But it’s a very extensible framework, so we can add support quite easily. First, we define AsyncController:

public class AsyncController : Controller
{
internal AsyncCallback Callback { get; set; }
internal IAsyncResult Result { get; set; }
internal Action<IAsyncResult> OnCompletion { get; set; }
 
protected void RegisterAsyncTask(Func<AsyncCallback, IAsyncResult> beginInvoke, Action<IAsyncResult> endInvoke)
{
OnCompletion = endInvoke;
Result = beginInvoke(Callback);
}
}

It’s just like a normal Controller, except it manages some internal state to do with asynchronous processing, and has the RegisterAsyncTask() method that lets you manage transitions across the gap between the two halves of IHttpAsyncHandler processing. Note: it doesn’t implement IHttpAsyncHandler itself; that’s the job of the IRouteHandler we have to set up. Unfortunately I had to reproduce most of the code from the framework’s MvcHandler, because I couldn’t just override any individual method and still get at the controller:

public class AsyncMvcRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new AsyncMvcHandler(requestContext);
}
 
class AsyncMvcHandler : IHttpAsyncHandler, IRequiresSessionState
{
RequestContext requestContext;
AsyncController asyncController;
HttpContext httpContext;
 
public AsyncMvcHandler(RequestContext context)
{
requestContext = context;
}
 
// IHttpHandler members
public bool IsReusable { get { return false; } }
public void ProcessRequest(HttpContext httpContext) { throw new NotImplementedException(); }
 
// IHttpAsyncHandler members
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
{
// Get the controller type
string controllerName = requestContext.RouteData.GetRequiredString("controller");
 
// Obtain an instance of the controller
IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
IController controller = factory.CreateController(requestContext, controllerName);
if (controller == null)
throw new InvalidOperationException("Can't locate the controller " + controllerName);
try
{
asyncController = controller as AsyncController;
if (asyncController == null)
throw new InvalidOperationException("Controller isn't an AsyncController.");
 
// Set up asynchronous processing
httpContext = HttpContext.Current; // Save this for later
asyncController.Callback = cb;
(asyncController as IController).Execute(new ControllerContext(requestContext, controller));
return asyncController.Result;
}
finally
{
factory.DisposeController(controller);
}
}
 
public void EndProcessRequest(IAsyncResult result)
{
CallContext.HostContext = httpContext; // So that RenderView() works
asyncController.OnCompletion(result);
}
}
}

It handles requests by supplying an AsyncMvcHandler, which implements IHttpAsyncHandler. Note that during the first half of the processing, i.e. during BeginProcessRequest(), it makes a record of the current HttpContext object. We have to restore that later, during EndProcessRequest(), because we’ll be in a new thread context by then and HttpContext will be null (and that breaks various ASP.NET facilities including WebForms view rendering).

Using AsyncController

It’s now very easy to handle a request asynchronously. Define a route using AsyncMvcRouteHandler, instead of MvcRouteHandler:

routes.Add(new Route("Default.aspx", new AsyncMvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }),
});

Then set up an AsyncController. In this example, we’re calling an external web service using the WebRequest class and its BeginGetResponse() method. That uses an IOCP, so won’t consume an ASP.NET worker thread while it waits:

public class HomeController : AsyncController
{
public void Index()
{
WebRequest req = WebRequest.Create("http://www.example.com");
req.Method = "GET";
 
RegisterAsyncTask(cb => req.BeginGetResponse(cb, null), delegate(IAsyncResult result) {
WebResponse response = req.EndGetResponse(result);
// Do something with the response here if you want
RenderView("Index");
});
}
}

And that’s it. The web request gets set up and started in the first half of the async processing model, then the thread gets released to serve other requests. When the web request signals completion, ASP.NET takes a different thread from its pool, and gets it to run the code inside the anonymous delegate, inheriting the request context from the first thread so it can send output to the visitor.

Just remember that you should only use async requests when you’re waiting for some operation on an IOCP. Don’t use it if you just going to call one of your own delegates asynchronously (e.g. using QueueUserWorkItem()) because that will come out of the ASP.NET worker thread pool and you’ll get exactly zero benefit (but more overhead).