Microsoft .NET Pet Shop 4:将 ASP.NET 1.1 应用程序迁移到 2.0

摘要:.NET Pet Shop 应用程序的设计说明了构建企业 n 层 .NET 2.0 应用程序的最佳做法,这种应用程序可能需要支持各种数据库平台和部署方案。
单击此处下载 .NET Pet Shop 4.0.msi。
本页内容概述
工作效率
从 ASP.NET 1.1 迁移到 2.0
体系结构
抽象工厂模式
用户界面增强
加密配置信息
模型对象
Order 和 Inventory 架构
Profile 数据库架构
小结

概述.NET Pet Shop 应用程序的设计说明了构建企业 n 层 .NET 2.0 应用程序的最佳做法,这种应用程序可能需要支持各种数据库平台和部署方案。
.NET Pet Shop 4 项目的目标是:
  • 工作效率:减少了 .NET Pet Shop 3 的代码数量 - 我们减少了近 25% 的代码。
  • ASP.NET 1.1 迁移到 2.0 利用 ASP.NET 2.0 的新功能 - 我们利用母版页、成员身份和配置文件,并设计出一个新的、吸引人的用户界面。

    1. .NET Pet Shop 4.0
  • 企业体系结构:构建一个灵活的最佳做法应用程序 - 我们实现了设计模式,以及表示层、业务层和数据层的分离。
返回页首

工作效率与 .NET Pet Shop 3 相比,.NET Pet Shop 4 中的代码量约减少了近 25%。减少代码行数的主要好处体现在表示层和数据访问层。
在表示层,我们减少了大约 25% 的代码。登录和签出步骤比完整的 ASP.NET 页面更简洁,需要的代码和 html 更少。这是因为向导控件本身处理过程流代码。使用母版页意味着使用较少的 html 代码和用户控件管理布局。相比于 Pet Shop 3 用户管理代码,成员身份服务处理身份验证的方式更简洁。
我们看到数据层节省的代码量最多,高达 36%。ASP.NET 2.0 SQL 成员身份提供程序取代了帐户管理代码。
表 1 给出逐层分解的完整代码量。
表 1. .NET Pet Shop 版本 3 与版本 4 的代码量对比

v3
v4
表示层
1,822
1,365
模型
349
395
业务逻辑层
210
199
数据访问层
1,538
985
代码总行数
3,919
2,944
[/table]图 2 对此做了进一步的图解。

2. 代码量对比图
.NET Pet Shop 4 引入了几个新功能,包括自定义的 ASP.NET 2.0 配置文件提供程序,以及通过 MSMQ 进行的异步定单处理等。表 2 显示新功能的代码数量:
表 2. .NET Pet Shop 4 新功能的代码量


自定义配置文件
853
Oracle 成员身份
586
缓存依赖项
90
消息队列
147
代码总行数
1,676
[table]返回页首

从 ASP.NET 1.1 迁移到 2.0为了实现 .NET Pet Shop 4 的目标,我们制定了下列计划:
  • 使用项目转换向导将 .NET Pet Shop 3.2 代码基从 ASP.NET 1.1 移植到 ASP.NET 2.0。
  • 规划我们想要包括的 ASP.NET 2.0 功能。
  • 实现一个支持这些功能的 n 层体系结构。
项目转换向导
首先,Visual Studio.NET 2005 项目转换向导迅速升级 .NET Pet Shop 3.2 代码基。通过这一基本移植,我们能够初步了解经过编译并在 ASP.NET 2.0 上运行的 .NET Pet Shop 3.2。
版本 3 和版本 4 之间的变化
通过升级 .NET Pet Shop 3.2 代码基以便在.NET Framework 2.0 上运行以及对 ASP.NET 2.0 的研究,我们推出了要在 .NET Pet Shop 4.0 中实现的以下主要功能:
  • System.Transactions 代替服务组件。
  • 用强类型集合的泛型代替松散类型的 ILists。
  • ASP.NET 2.0 成员身份,用于用户身份验证和授权。
  • 用于 Oracle 10G 的自定义 ASP.NET 2.0 成员身份提供程序。
  • ASP.NET 2.0 自定义 Oracle 和 SQL Server 配置文件提供程序,用于用户状态管理。
  • 用母版页取代 ASP.NET Web 用户控件,从而获得一致的外观。
  • ASP.NET 2.0 向导控件。
  • 使用 SqlCacheDependency(而非基于超时)的数据库级缓存失效。
  • 启用基于消息队列构建的异步 Order 处理。
什么是 System.Transactions
System.Transactions 是 .NET 2.0 框架中新增的事务控件命名空间。它是一种处理分布式事务的新方式,没有 COM+ 注册和 COM+ 目录的开销。请注意,Microsoft 分布式事务协调器用于初始化事务。
运行情况
同步定单处理中的 Order.Insert() 方法使用 System.Transactions 插入一个定单并更新库存。通过添加对 System.Transaction 命名空间的引用,并将定单插入方法和库存减少方法包装在 TransactionScope 内,我们实现了 Order.Insert() 方法,如代码清单 1 所示。
清单 1. 运行中的 System.Transactions
using System;
using System.Transactions;
using PetShop.IBLLStrategy;

namespace PetShop.BLL {
    /// <summary>
    /// This is a synchronous implementation of IOrderStrategy
    /// By implementing IOrderStrategy interface, the developer can
    /// add a new order insert strategy without re-compiling the whole
    /// BLL.
    /// </summary>
    public class OrderSynchronous : IOrderStrategy {
      ...
        /// <summary>
        /// Inserts the order and updates the inventory stock within
        /// a transaction.
        /// </summary>
        /// <param name="order">All information about the order</param>
        public void Insert(PetShop.Model.OrderInfo order) {

            using (TransactionScope ts = new                     
TransactionScope(TransactionScopeOption.Required)) {

                dal.Insert(order);

                // Update the inventory to reflect the current inventory
                // after the order submission.
                Inventory inventory = new Inventory();
                inventory.TakeStock(order.LineItems);

                // Calling Complete commits the transaction.
                // Excluding this call by the end of TransactionScope's
                // scope will rollback the transaction.
                ts.Complete();
            }
        }
    }
}
在 .NET Pet Shop 3 中,分布式事务由企业服务处理,需要 COM+ 注册。OrderInsert 类从服务组件派生,事务由 COM+ 处理。然后,服务组件使用 regsvr32 命令进行注册。
清单 2. Pet Shop 3 的定单插入
using System;
using System.Collections;
using System.EnterpriseServices;
using System.Runtime.InteropServices;
...
namespace PetShop.BLL {
  /// <summary>
  /// A business component to manage the creation of orders
  /// Creation of an order requires a distributed transaction
  /// so the Order class derives from ServicedComponents
  /// </summary>
  [Transaction(System.EnterpriseServices.TransactionOption.Required)]
  [ClassInterface(ClassInterfaceType.AutoDispatch)]
  [ObjectPooling(MinPoolSize=4, MaxPoolSize=4)]
  [Guid("14E3573D-78C8-4220-9649-BA490DB7B78D")]
  public class OrderInsert : ServicedComponent {
      ...
      /// <summary>
      /// A method to insert a new order into the system
      /// The orderId will be generated within the method and should not
      /// be supplied as part of the order creation the inventory will be
      /// reduced by the quantity ordered.
      /// </summary>
      /// <param name="order">All the information about the order</param>
      /// <returns>
      /// The new orderId is returned in the order object
      /// </returns>
      [AutoComplete]
      public int Insert(OrderInfo order) {

        // Get an instance of the Order DAL using the DALFactory
        IOrder dal = PetShop.DALFactory.Order.Create();

        // Call the insert method in the DAL to insert the header
        int orderId = dal.Insert(order);

        // Get an instance of the Inventory business component
        Inventory inventory = new Inventory();
        inventory.TakeStock( order.LineItems);
        ...

        // Set the orderId so that it can be returned to the caller
        return orderId;
      }
  }
}
System.Transactions 的优点
从企业服务移动到 System.Transactions 可以简化部署,因为后者不需要使用 COM+ 目录。使用 COM+ 目录时,我们忽略了其他一些额外的功能,只保留了分布式事务支持。System.Transaction 使得在 ASP.NET 2.0 应用程序中编程和部署分布式应用程序变得十分简单。System.Transactions 在运行时的性能提高了 50%,因为它避免了对象实例化的 COM+ 目录查找所产生的开销。最后一个优点是,针对 SQL Server 2005 运行时,System.Transactions 能够检测到某个分布式事务何时针对宿主在一个 SQL Server 2005 实例上的两个不同数据库运行。在这种情况下,它能够将该分布式事务提升为一个本地事务,这样就可避免与分布式事务登录/两阶段提交相关的全部开销,从而极大地提高性能。
泛型
什么是泛型?
每次返回一个 Pet Shop 模型对象集合时,我们都针对该对象使用泛型类型的一个集合列表。这是 C# 2.0 的一个新增功能,称之为泛型。
运行情况
我们可以从清单 3 所示的 GetProductsByCategory 方法中看到泛型的运行情况。
清单 3. Product.cs (Pet Shop 4.0)
      /// <summary>
      /// A method to retrieve products by category name
      /// </summary>
      /// <param name="order">The category name to search by</param> 
      /// <returns>A Generic List of ProductInfo</returns>
      public IList<ProductInfo>GetProductsByCategory(string category) {

        // Return new if the string is empty
        if (string.IsNullOrEmpty(category))
            return new List<ProductInfo>();

        // Run a search against the data store
        return dal.GetProductsByCategory(category);
}
以下是 Pet Shop 3 中的等效代码,它返回一个 IList
清单 4. Product.cs (Pet Shop 3)
      /// <summary>
      /// A method to retrieve products by category name
      /// </summary>
      /// <param name="order">The category name to search by</param>
      /// <returns>
      /// An interface to an arraylist of the search results
      /// </returns>
      public IList GetProductsByCategory(string category) {

        // Return null if the string is empty
        if (category.Trim() == string.Empty)
            return null;

        // Get an instance of the Product DAL using the DALFactory
        IProduct dal = PetShop.DALFactory.Product.Create();

        // Run a search against the data store
        return dal.GetProductsByCategory(category);
      }
泛型的优点
泛型允许我们返回对象的强类型集合,而不是 .NET Pet Shop 3 中的 IList 集合。泛型强类型集合提供类型安全,其性能优于普通的集合。另外,泛型强类型集合将在 Visual Studio 2005 Intellisense 中出现,这可以提高开发人员工作效率。
ASP.NET 2.0 成员身份
成员身份提供一个通用的用户身份验证和管理框架。当用户信息存储在 SQL Server 中时,.NET Pet Shop 4 使用 SQL Server 成员身份提供程序;当用户信息存储在 Oracle 中时,.NET Pet Shop 4 使用自定义成员身份提供程序。
运行情况
要在 .NET Pet Shop 4 中实现成员身份,需要执行以下步骤:
  • 配置窗体身份验证。
    <authentication mode="Forms">
            <forms name="PetShopAuth" loginUrl="SignIn.aspx"
                    protection="None" timeout="60"/>
          </authentication>
  • 要使用 SQL 成员身份提供程序,我们必须安装成员身份数据库。成员身份数据库是在运行下列命令时由 ASP.NET 创建的。
    %WinDir%\Microsoft.NET\Framework\<.NET version>\aspnet_regsql
    -S <server\instance> -E -A all -d MSPetShop4Services
  • 配置 SQL 成员身份提供程序。
    <membership defaultProvider="SQLMembershipProvider">
            <providers>
                <add name="SQLMembershipProvider"
                    type="System.Web.Security.SqlMembershipProvider"
                    connecti
                    applicati
                    enablePasswordRetrieval="false"
                    enablePasswordReset="true"
                    requiresQuesti
                    requiresUniqueEmail="false"
                    passwordFormat="Hashed"/>
            </providers>
          </membership>
  • ASP.NET Login 控件封装所有登录逻辑。CreateUserWizard 控件处理新的用户注册。
ASP.NET 2.0 成员身份的优点
通过成员身份服务,我们能够使用预建的用户身份验证和注册控件,而无需从头编写这些控件。最终结果是:为登录、登录状态、用户身份、用户注册和密码恢复编写的代码变少了。
而且,由于成员身份现在驻留在自己的数据库中,因此我们能够删除 .NET Pet Shop 3 中使用的 Accounts 表,然后使用 ASP.NET 2.0 创建的成员身份服务数据库。
用于 Oracle 10G 的自定义成员身份提供程序
.NET 2.0 框架包括一个 SQL Server 成员身份提供程序。为了在应用程序使用 Oracle 成员身份数据库时保留用户帐户,我们为 Oracle 创建了一个自定义成员身份提供程序实现。我们仅实现了由 .NET Pet Shop 4 使用的方法,即 CreateUser 方法和 Login 方法。然而,任何希望将 Oracle 10G ASP.NET 成员身份服务一起使用的用户都可以使用和 / 或扩展该代码
运行情况
CreateUser 方法是 MembershipProvider 类的实现的方法之一。它深入探究 OracleMembershipProvider 的工作方式。
清单 5. OracleMembershipProvider.cs CreateUser(...)
using System;
using System.Configuration.Provider;

namespace PetShop.Membership {
class OracleMembershipProvider : MembershipProvider {
     
       
    string password, string email, string passwordQuestion,
    string passwordAnswer, bool isApproved, object userId,
    out MembershipCreateStatus status) {
 
  // create connection
  OracleConnection connection =
      new OracleConnection(OracleHelper.ConnectionStringMembership);
  connection.Open();
  OracleTransaction transaction = 
      connection.BeginTransaction(IsolationLevel.ReadCommitted);

  try {
      DateTime dt = DateTime.Now;
      bool isUserNew = true;

      // Step 1: Check if the user exists in the Users
      // table: Create if not     
      int uid = GetUserID(transaction, applicationId, username, true,
                          false, dt, out isUserNew);
      if (uid == 0) { // User not created successfully!
        status = MembershipCreateStatus.ProviderError;
        return null;
      }

      // Step 2: Check if the user exists in the Membership table: Error
      // if yes
      if (IsUserInMembership(transaction, uid)) {
        status = MembershipCreateStatus.DuplicateUserName;
        return null;
      }

      // Step 3: Check if Email is duplicate
      if (IsEmailInMembership(transaction, email, applicationId)) {
        status = MembershipCreateStatus.DuplicateEmail;
        return null;
      }

      // Step 4: Create user in Membership table             
      int pFormat = (int)passwordFormat;
      if (!InsertUser(transaction, uid, email, pass, pFormat, salt, "",
                      "", isApproved, dt)) {
        status = MembershipCreateStatus.ProviderError;
        return null;
      }

      // Step 5: Update activity date if user is not new
      if(!isUserNew) {
        if(!UpdateLastActivityDate(transaction, uid, dt)) {
            status = MembershipCreateStatus.ProviderError;
            return null;
        }
      }

      status = MembershipCreateStatus.Success;

      return new MembershipUser(this.Name, username, uid, email,
                                passwordQuestion, null, isApproved,
                                false, dt, dt, dt, dt, DateTime.MinValue);
  }
  catch(Exception) {
      if(status == MembershipCreateStatus.Success)
        status = MembershipCreateStatus.ProviderError;
      throw;
  }
  finally {
      if(status == MembershipCreateStatus.Success)
        transaction.Commit();
      else
        transaction.Rollback();
     
      connection.Close();
      connection.Dispose();
  }
}
未实现的方法成为空存根,如下所示:
public override string GetUserNameByEmail(string email) {
  throw new Exception("The method or operation is not implemented.");
}
Oracle 10G 成员身份提供程序的优点
由于我们希望 .NET Pet Shop 4 将成员身份数据存储在 Oracle 数据库以及 SQL Server 中,因此我们实现了一个自定义的成员身份提供程序。提供程序模型使我们能够将 Oracle 数据库与 ASP.NET 2.0 成员身份服务进行简单、快速地集成。
ASP.NET 2.0 配置文件
在 ASP.NET 2.0 中,可跨越多个 Web 应用程序,将用户信息存储到一个名为 Profile 的新服务中。.NET Pet Shop 4 的配置文件实现存储并检索用户的购物车、购物清单和帐户信息。这里的关键的一点是,很多用户会发现,假如为用户会话信息提供一个事务处理、集群安全的存储,这几乎可以完全替换他们对会话对象的使用。默认情况下,配置文件服务将数据序列化为一个 BLOB,存储在数据库中。但是,通过实现您自己的配置文件服务序列化服务可以获得更高的性能。对于 Pet Shop 4,创建了一个配置文件服务的自定义实现来降低序列化开销。
运行情况
  • 配置配置文件提供程序。
    清单 6. 配置文件提供程序配置
    <profile automaticSaveEnabled="false"
    defaultProvider="ShoppingCartProvider">
      <providers>
          <add name="ShoppingCartProvider"
              connecti
              type="PetShop.Profile.PetShopProfileProvider"
              applicati/>
          <add name="WishListProvider"
              connecti
              type="PetShop.Profile.PetShopProfileProvider"
              applicati/>
          <add name="AccountInfoProvider"
              connecti
              type="PetShop.Profile.PetShopProfileProvider"
              applicati/>
      </providers>
      <properties>
          <add name="ShoppingCart" type="PetShop.BLL.Cart"
              allowAn provider="ShoppingCartProvider"/>
          <add name="WishList" type="PetShop.BLL.Cart"
              allowAn
              provider="WishListProvider"/>
          <add name="AccountInfo" type="PetShop.Model.AddressInfo"
              allowAn provider="AccountInfoProvider"/>
      </properties>
    </profile>
  • 迁移匿名配置文件。
    清单 7. 迁移匿名配置文件
    // Carry over profile property values from an anonymous to an
    // authenticated state
    void Profile_MigrateAnonymous(Object sender, ProfileMigrateEventArgs e) {
        ProfileCommon anonProfile = Profile.GetProfile(e.AnonymousID);

        // Merge anonymous shopping cart items to the authenticated
        // shopping cart items
        foreach (CartItemInfo cartItem in
            anonProfile.ShoppingCart.CartItems)
            Profile.ShoppingCart.Add(cartItem);

        // Merge anonymous wishlist items to the authenticated wishlist
        // items
        foreach (CartItemInfo cartItem in anonProfile.WishList.CartItems)
            Profile.WishList.Add(cartItem);

        // Clean up anonymous profile
        ProfileManager.DeleteProfile(e.AnonymousID);
        AnonymousIdentificationModule.ClearAnonymousIdentifier();
       
        // Save profile
        Profile.Save();
    }
    清单 8. 购物车
    using System;
    using System.Collections.Generic;
    using PetShop.Model;

    namespace PetShop.BLL {

        /// <summary>
        /// An object to represent a customer's shopping cart.
        /// This class is also used to keep track of customer's wish list.
        /// </summary>
        [Serializable]
        public class Cart {

            // Internal storage for a cart   
            private Dictionary cartItems =
                new Dictionary();

            /// <summary>
            /// Calculate the total for all the cartItems in the Cart
            /// </summary>
            public decimal Total {
                get {
                    decimal total = 0;
                    foreach (CartItemInfo item in cartItems.Values)
                        total += item.Price * item.Quantity;
                    return total;
                }
            }

            /// <summary>
            /// Update the quantity for item that exists in the cart
            /// </summary>
            /// Item Id
            /// Quantity
            public void SetQuantity(string itemId, int qty) {
                cartItems[itemId].Quantity = qty;
            }

            /// <summary>
            /// Return the number of unique items in cart
            /// </summary>
            public int Count {
                get { return cartItems.Count; }
            }

            /// <summary>
            /// Add an item to the cart.
            /// When ItemId to be added has already existed, this method
            /// will update the quantity instead.
            /// </summary>
            /// Item Id of item to add
            public void Add(string itemId) {
                CartItemInfo cartItem;
                if (!cartItems.TryGetValue(itemId, out cartItem)) {
                    Item item = new Item();
                    ItemInfo data = item.GetItem(itemId);
                    if (data != null) {
                        CartItemInfo newItem = new CartItemInfo(itemId,
                            data.ProductName, 1, (decimal)data.Price,
                            data.Name, data.CategoryId, data.ProductId);
                            cartItems.Add(itemId, newItem);
                    }
                }
                else
                    cartItem.Quantity++;
            }

            /// <summary>
            /// Add an item to the cart.
            /// When ItemId to be added has already existed, this method
            /// will update the quantity instead.
            /// </summary>
            /// Item to add
            public void Add(CartItemInfo item) {
                CartItemInfo cartItem;
                if (!cartItems.TryGetValue(item.ItemId, out cartItem))
                    cartItems.Add(item.ItemId, item);
                else
                    cartItem.Quantity += item.Quantity;
            }

            /// <summary>
            /// Remove item from the cart based on itemId
            /// </summary>
            /// ItemId of item to remove
            public void Remove(string itemId) {
                cartItems.Remove(itemId);
            }

            /// <summary>
            /// Returns all items in the cart. Useful for looping through
            /// the cart.
            /// </summary>
            /// Collection of CartItemInfo
            public ICollection CartItems {
                get { return cartItems.Values; }
            }

            /// <summary>
            /// Method to convert all cart items to order line items
            /// </summary>
            /// A new array of order line items
            public LineItemInfo[] GetOrderLineItems() {

                LineItemInfo[] orderLineItems =
                    new LineItemInfo[cartItems.Count];
                int lineNum = 0;

                foreach (CartItemInfo item in cartItems.Values)
                    orderLineItems[lineNum] = new LineItemInfo(item.ItemId,
                        item.Name, ++lineNum, item.Quantity, item.Price);

                return orderLineItems;
            }

            /// <summary>
            /// Clear the cart
            /// </summary>
            public void Clear() {
                cartItems.Clear();
            }
        }
    }
ASP.NET 2.0 配置文件的优点
使用 ASP.NET 2.0,用户的购物车可以存储在数据库中并持久保留,这样,如果用户两三天后再回来,他们仍然拥自己的购物车。此外,配置文件服务是"按需"提供的,而会话状态对象对于任何引用它的页面,每页都要进行重新加载;配置文件服务的一个优势是只在实际需要时才加载。
而且,使用配置文件功能,我们能够从现有的 Pet Shop 3 数据库中删除 Account 表和 Profile 表,这样也会减少业务逻辑层和数据访问层中的代码量。
母版页
ASP.NET 2.0 提供一种通过使用母版页保持整个 Web 站点外观一致的新技术。.NET Pet Shop 4 母版页包含标头、LoginView 控件、导航菜单和呈现内容的 HTML。所有其他 Pet Shop Web 窗体都使用 Pet Shop 4 母版页。
运行情况
图 3 展示 .NET Pet Shop 4 母版页。

3. .NET Pet Shop 4 母版页
清单 9. 绑定母版页
<%@ Page AutoEventWireup="true" Language="C#"
        MasterPageFile="~/MasterPage.master" Title="Products"
        Inherits="PetShop.Web.Products" CodeFile="~/Products.aspx.cs" %>
ASP.NET 2.0 母版页的优点
使用母版页,我们能够只创建一种布局,然后即可对所有 .NET Pet Shop 页重用该布局。开发期间对该布局的任何更改都直接作用于母版页,其他页面更改的只涉及到内容。相反,通过将标头和导航栏封装在名为 NavBar.ascx 的 ASP.NET 用户控件中,可以实现 .NET Pet Shop 3 中的用户界面。.NET Pet Shop 3 中的每个 Web 窗体都包含用于控制布局的 NavBar 用户控件和 HTML。更改布局将涉及处理每个 Web 窗体上的 NavBar 用户控件,或修改每个 Web 窗体上的 HTML。
ASP.NET 2.0 向导控件
.NET Pet Shop 4 中的签出过程包含在 CheckOut 页面上的一个 Wizard 控件中。Wizard 是一个新控件,它提供一种实现逐步过程的新方法。Wizard 控件管理窗体间的导航、数据持久性以及每一步的状态管理。
运行情况

4. 签出向导控件
ASP.NET 2.0 向导控件的优点(单击图像查看大图像)
.NET Pet Shop 3 中的签出过程涉及一系列互相通信的 ASP.NET 页面。从购物车页面,用户可以转到签出页面;在签出页面,用户输入其账单信息,最后系统处理定单。该流程由一个名为 CartController 的自定义类控制,它使用会话状态管理步骤间的通信。

5. .NET Pet Shop 3 签出过程
使用 Wizard 控件,只需要较少的代码即可在 .NET Pet Shop 4 中实现签出,从而使该过程变得十分简单。
数据库级缓存失效
SQL Cache Dependency 是 ASP.NET 2.0 的一个新增对象,可用于在 SQL Server 中的数据更改时使缓存失效。Pet Shop 4 使用 SQL Cache Dependency 对象使目录、产品和项目缓存失效。
就现成可用的功能而言,Pet Shop 仅包含基于表的缓存依赖项实现。开发人员可以通过扩展 CacheDependency 对象来实现自己的缓存失效机制。机制实现后,就可以从 Web.config 配置 Pet Shop 4 的 CacheDependency
请注意,Pet Shop 4 的 SQL CacheDependency 仅仅是为在 SQL Server 上运行而设计的。对于 Oracle,.NET Pet Shop 4 将回到基于时间的缓存过期。
运行情况
图 6 显示 SQL Server 的缓存依赖项:

6. Pet Shop 表缓存依赖项
数据库级缓存失效的优点
使用缓存失效,我们可以使显示的内容与 Pet Shop 数据库中的数据保持一致,但是仍能实现中间层对象缓存的优势,从而降低中间层上的运行时处理要求,以及减少数据库调用。这样可以提高应用程序的可伸缩性(它可以处理更多并发用户),同时还可降低数据库负载。
异步 Order 处理
我们作的另一处更改是添加了一个选项,以配置定单过程应该将事务直接(同步)提交给数据库,还是应该提交给指定的队列,稍后再对该队列中的定单进行处理(异步)。在异步定单处理过程中,如果用户提交了一个定单,该定单将进入一个队列。.NET Pet Shop 4 有一个要存储在 Microsoft 消息处理队列 (MSMQ) 中的实现。稍后,该定单队列可以由 Order 处理器控制台应用程序处理。该方法的一个优势是,定单数据库甚至不必进行客户运行,就能够处理定单。由于 MSMQ 使用持久队列,因此无需用户干涉仍然能够捕获所有定单,一旦处理应用程序和定单数据库再次上线,所有定单都将插入数据库中。
运行情况
为了处理同步和异步定单处理之间的算法改变,我们使用策略模式。在策略模式中,发出定单的方法是从 BLL.Order.Insert 方法中分离出来的。根据 OrderStrategy 的 Web.config 设置,使用了对应的 Insert 方法。默认情况下,.NET Pet Shop 配置为同步运行。
要配置 Order 策略,将 OrderStrategyClass 值从 OrderSynchronous 更改为 OrderAsynchronous。此外,对于异步定单处理,MSMQ 必须通过一个为 Pet Shop 而创建的私有队列来启用,如下所示。
<add key="OrderStrategyClass" value="PetShop.BLL.OrderSynchronous"/>
      <add key="OrderQueuePath" value="private queue path"/>
同步发出定单
图 7 说明同步发出定单。当用户签出定单时,签出按钮单击事件处理程序调用 BLL 中的 Order Insert 方法。对于同步发出定单,BLL Order 对象使用 OrderSynchronous Insert 方法将新定单插入 Orders 数据库中,然后更新 Inventory 数据库以反映定单提交后的当前库存。

7. 同步发出定单
异步发出定单
图 8 说明异步发出定单。在 Web 站点上,如果用户单击 CheckOut 按钮,就会调用 BLL Order Insert 方法。然而,由于 OrderStrategy 是针对异步配置的,所以使用 OrderAsynchronous 策略。OrderAsynchronous 插入方法直接将定单信息发送到队列中。

8. 异步发出
定单处理器
定单处理器是一个控制台应用程序,我们创建它的目的是接收消息处理实现中的定单,并将这些定单转录到 Order 数据库和 Inventory 数据库中。定单处理器以多线程方式运行,以批处理方式处理定单。它重用同步定单策略将新定单插入到 Orders 数据库中,并减少 Inventory 数据库中的值。
异步定单处理的优点
其他许多企业应用程序也使用了异步定单处理。要想使 .NET Pet Shop 4 在定单以多线程方式处理时性能更佳,分离定单过程不失为一种方法。
返回页首

体系结构对于早期版本的 .NET Pet Shop,体系结构重点关注用户界面、应用程序逻辑和数据之间的完全分离。这一完全分离允许我们更改一个层的实现,而不会影响其他层。例如,我们可以更改数据库供应商,而不必更改业务逻辑代码。
图 9 中的图表说明 .NET Pet Shop 4 的高级逻辑体系结构。表示层 (WEB) 包含各种用户界面元素。业务逻辑层 (BLL) 包含应用程序逻辑和业务组件。数据访问层 (DAL) 负责与数据库交互,进行数据存储和检索。以下各部分中将对各层进行详细讨论。

9. .NET Pet Shop 4 的体系结构图(单击图像查看大图像)
返回页首

抽象工厂模式.NET Pet Shop 4 使用抽象工厂设计模式,该模式中的接口用于创建一系列相关或依赖的对象,而无需指定其具体类。数据访问层中有一个该模式的示例,其中包括针对 IDAL、DAL 工厂、Oracle DAL 和 SQL Server DAL 的项目。为缓存、库存和定单数据访问、消息处理,以及配置文件数据访问创建抽象工厂。
表示层
ASP.NET 2.0 包括许多可以提高开发人员工作效率的内置功能。构建 .NET Pet Shop 4 时,我们重新设计了用户界面,从而可以利用 ASP.NET 2.0 提供的新功能,如母版页、主题、皮肤、Wizard 控件和 Login 控件。为了保留用户帐户,我们利用成员身份提供程序(而不是使用 ASP.NET 会话状态)存储用户的购物车和喜欢的产品。新的配置文件提供程序可以存储可使编程和管理用户状态更加简单的强类型购物车。使用所有这些功能,我们能够快速实现 Pet Shop 表示层更改。
返回页首

用户界面增强.NET Pet Shop 4 彻底地呈现出一种新外观。新的用户界面支持更大的宠物目录,使用户可以更容易地查找和购买各种新宠物。更改 .NET Pet Shop 用户界面的外观后,我们对 .NET Pet Shop 提供的示例宠物很感兴趣。.NET Pet Shop 中现在有企鹅、小虫、熊猫,甚至骨骼、恐龙和透明的小猫!通过添加购物清单、浏览途径记录以及其他小功能,我们还可以改善购物体验。
返回页首

加密配置信息.NET Framework 2.0 引入了一个受保护的配置功能,我们可以使用该功能加密连接字符串。使用该功能,我们可以加密敏感的数据库用户名和密码信息。
当您选择"full source and database install"选项时,.NET Pet Shop 安装程序将自动运行一段脚本,以加密 Web.config 文件中存储的连接字符串。
要在"source only"安装上执行配置加密,运行安装目录中的 EncryptWebConfig.bat 文件。
C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\ASPNET_regiis.exe
    -pef "connectionStrings"
    "C:\Program Files\Microsoft\.NET Pet Shop 4.0\Web"
业务逻辑层
.NET Pet Shop 4 的业务逻辑保留了 .NET Pet Shop 3 的大部分业务逻辑,如 Model 对象及其使用方式。为数不多的更改包括使用泛型,异步发出定单,以及 System.Transactions 命名空间。
返回页首

模型对象.NET Pet Shop 4 保留了 .NET Pet Shop 3 中的 Model 对象。这些对象是模仿数据库表结构的自定义轻量级类。它们在各应用程序层之间共享以相互通信。例如,如果返回一个目录中的多个产品,我们就返回一个 Product Model 对象集合。
数据访问层
BLL 与数据访问层通信以访问 Pet Shop 4 数据库中的数据。.NET Pet Shop 4 使用以下四个数据库:Inventory、Orders、Membership 和 Profile。对于 .NET Pet Shop 3,该版本支持 Oracle 和 SQL Server 数据库。
返回页首

Order 和 Inventory 架构.NET Pet Shop 4 中使用的 Orders 和 Inventory 的数据库架构是从 .NET Pet Shop 3 移植而来的。删除了几个未使用的字段。该数据库具有以下表的总体结构:

10. Pet Shop Orders 数据库

11. Pet Shop Inventory 数据库
返回页首

Profile 数据库架构Profile 数据库用于存储特定于用户的信息,如帐户信息和购物车内容。该数据库具有以下表的总体结构:

12. Pet Shop Profile 数据库
返回页首

小结Microsoft .NET Pet Shop 4.0 应用程序用来重点说明构建可伸缩企业 Web 应用程序所使用的关键技术和体系结构。由于 ASP.NET 2.0 中的增强功能,我们能够更快速地构建 n 层企业应用程序,这使我们可以花时间构建更丰富、功能更全面的 Pet Shop。
.NET 2.0 的主要更改和新增功能包括:
  • System.Transactions 允许更快速的事务处理和更简单的部署,而无需使用 COM+ 目录。
  • 泛型:允许我们返回对象的强类型集合而不是 .NET Pet Shop 3 中的 IList 集合。支持更简单的编码,因为智能感知会识别集合中的类型对象。
  • ASP.NET 2.0 成员身份服务:提供了通用的用户身份验证和管理框架,可以极大地减少与创建和维护用户帐户信息相关的代码量。允许我们使用预建的用户身份验证和注册控件,而无需从头编写这些控件。最终结果是可以为登录、登录状态、用户身份、用户注册和密码恢复编写更少的代码。
  • ASP.NET 2.0 配置文件服务:为特定于用户的信息(如购物车)替换会话对象的使用。为用户会话信息提供了一个集群安全的事务处理存储,用户会话信息可以跨多个用户对站点的访问进行维护。
  • ASP.NET 2.0 母版页:提供了一种新技术以保持整个 Web 站点的外观一致,仅通过更新母版页,就可以轻松地将全局更改应用于站点多个页面的外观。
  • ASP.NET 2.0 向导控件:一个新的服务器端控件,它提供了一种实现逐步过程的简单方法。我们使用它来减少 Pet Shop 4.0 签出过程的编码量。
  • ASP.NET 2.0 SQL 缓存依赖项:允许中间层对象缓存在后端数据库信息更改时自动失效。对于 SQL 2000,它在表级别有效(如同在 Pet Shop 4 中一样),而对于 SQL Server 2005,它也可以在单独的行级别有效。利用该功能,缓存的数据库信息可以始终保持是最新的,同时仍利用缓存来降低中间层和数据库服务器上的负载。
  • 通过消息处理的异步定处理选项:虽然不是 .NET 2.0 的功能(.NET 1.1 也提供此功能),我们还是对 Pet Shop 4 进行了扩展,使其能够选择是通过 MSMQ 和 System.Messaging 使用消息处理,还是直接将标准的同步事务用于数据库。倘若有更强的可靠性和潜在的可伸缩性,这会将定单处理与定单数据库分离。
转到原英文页面
返回页首