数据访问组件&ORM测试框架(1)——EF方案实现

一、前言

首先,鉴于本文所展现的ORM耗时测试已成为了博友的吐嘈点,我想我有必要声明一点:我发布这个测试框架,相当于一个活动,目的是收集各种数据访问解决方案的实现示例,并在 性能,易用性,代码量 上做一个综合的对比,让大家更好的了解各个解决方案的优缺点,选择的时候更明确。时间的对比只是其中一个方面,并不是对比的全部。

最近又掀起了数据访问组件&ORM的性能比拼了,但基本都是各说各的好,没有一个统一的标准与平台来进行对比。

郭某不才,花了点时间写了个数据访问性能测试框架,主要是想各个ORM在一个统一的环境下完成相同的事,来进行一下公平的对比,看看哪家的性能更好、更易用、完成相同的业务代码量更少。

特别说明,本框架测试环境如下:VS2010+SP1或以上,SQL2005或以上,.NET 4.0,能运行以上环境的操作系统

本框架的代码已发布到 codeplex 上,同学们可以通过VS自带的团队项目管理功能(TFS)或SVN 随时获取最新代码。

项目地址:https://datatester.codeplex.com

TFS获取地址:https://tfs.codeplex.com:443/tfs/TFS17,(似乎只有团队成员可以用TFS方式)需要登录,并在项目的 SOURCE CODE 标签的 Connect 连接处获取VS端的登录用户名,密码为自己的账号密码。

SVN获取地址:https://datatester.svn.codeplex.com/svn

二、设计思路

这个测试框架主要是设计一个通用的测试基类,把要做的事规定好,并把实现细节开放出来供给各家ORM自己去实现,为公平起见,规定如下:

  1. 所有数据访问操作都从一个数据库连接字符串开始。

  2. 要实现的业务最终结果必须相同,比如有些同学给出的ado.net与ORM的对比测试,ORM查询出来的是一个实体,而ado.net去只是执行一条相应的sql语句,显然是不公平的,ado.net也必须构造出一个实体,才算把业务实现了。

  3. 统一的调用方案,所有的测试方法的调用入口相同。

根据这个设计思想,设计了单个实体与多个实体的添加,查询,修改,删除操作,为了体现各个ORM的易用性,还添加了一个比较复杂的查询操作。以上这些操作都是在基类中定义了对应的 protected abstract 的方法,需要在具体的实现类中进行实现。并且唯一的前提条件只有基类中的的一个只读的数据库连接字符串ConnectionString,如果该连接串不满足要求,也可以在实现类中进行重写。

本次测试使用到的数据库结构如下(偷下懒,直接上EF4.0的反向数据库功能生成的 edmx 图上)

由图可以看出,实体关系如下:

  1. 分类:一个产品分类可以对应多个产品

  2. 客户:一个客户可以对应多个订单

  3. 产品:一个产品必定对应一个分类,且可以对应多个订单明细

  4. 订单:一个订单必定对应一个客户,且可以对应多个订单明细

  5. 订单明细:一个订单明细必定对应一个产品与一个订单

在本框架中,规定的要实现的业务如下:

  • 单个操作

  1. 添加一个客户信息(客户名称做有唯一处理)

  2. 根据客户名称获取步骤1中添加的客户信息,并返回

  3. 将步骤2中获取的客户信息的 PostalCode 与 Tel 信息更新为指定字符串

  4. 删除这个客户信息

  • 批量操作

  1. 批量添加指定数量的客户信息(客户名称做有唯一处理)

  2. 查询出刚添加的指定数量的客户信息,并返回客户信息的集合

  3. 将步骤2获取的客户信息的 Address 信息更新为 Address + CustomerName

  4. 删除批量操作所添加的客户信息

  • 复杂查询

复杂查询将查询已完成的订单信息的集合,并以如下视图模型作为结果装载

视图模型定义如下:

namespace DataTestFramework.ViewModels
{
    /// <summary>
    /// 视图模型——订单信息
    /// </summary>
    public class OrderView
    {
        public int OrderId { get; set; }

        public DateTime OrderDate { get; set; }

        public decimal SumMoney { get; set; }

        public bool Finished { get; set; }

        /// <summary>
        /// 订单关联的客户名称
        /// </summary>
        public string CustomerName { get; set; }

        /// <summary>
        /// 当前订单的所有订单明细所关联的产品名称,多个以逗号分隔
        /// </summary>
        public string ProductNames { get; set; }
    }
}

三、核心代码分析

借助VS2012更新了Update1后的新功能 Code Map,测试基类的结构展现如下:

上图展示了测试基类TesterBase的调用结构:

  1. Work方法是公共方法,测试的运行由这个方法开始。

  2. SingleCrudTest、MultipleCrudTest、RetrieveComplex三个方法是私有方法,分别负责单个实体,多个实体,复杂查询的功能调用、计时与输出功能。

  3. 再下一级的方法是受保护的抽象方法,负责各个功能的业务实现,需要具体的实现类中进行实现。

测试基类 TesterBase 的具体代码如下:

namespace DataTestFramework.Infrastructure
{
    /// <summary>
    ///     测试类基类
    /// </summary>
    public abstract class TesterBase
    {
        private const string _connectionString = "Data Source=.; Integrated Security=True;" +
            " Initial Catalog=DataTestFramework; Pooling=True; MultipleActiveResultSets=True;";

        /// <summary>
        ///     获取 数据库连接字符串
        /// </summary>
        protected virtual string ConnectionString
        {
            get { return _connectionString; }
        }

        #region 受保护方法

        #region 单个操作

        /// <summary>
        ///     添加单个客户信息
        /// </summary>
        /// <param name="customer">待添加的客户信息</param>
        protected abstract void CreateCustomer(Customer customer);

        /// <summary>
        ///     获取指定名称的客户信息
        /// </summary>
        /// <param name="customerName">带Token的客户名称</param>
        /// <returns>指定名称的客户信息,不存在时返回null</returns>
        protected abstract Customer RetrieveCustomer(string customerName);

        /// <summary>
        ///     更新指定客户信息的 PostalCode="100000",Tel="13800138000"
        /// </summary>
        /// <param name="customer">待更新的客户信息</param>
        protected abstract void UpdateCustomer(Customer customer);

        /// <summary>
        ///     删除指定客户信息
        /// </summary>
        /// <param name="customerId">客户编号</param>
        protected abstract void DeleteCustomer(int customerId);

        #endregion

        #region 批量操作

        /// <summary>
        ///     批量添加客户信息
        /// </summary>
        /// <param name="customers">待添加的客户信息集合</param>
        protected abstract void CreateCustomers(IEnumerable<Customer> customers);

        /// <summary>
        ///     反向(先倒序排序)获取指定数量的客户信息
        /// </summary>
        /// <param name="count">要获取的数量</param>
        /// <returns>获取的客户信息集合</returns>
        protected abstract IEnumerable<Customer> RetrieveCustomers(int count);

        /// <summary>
        ///     更新指定的多个客户信息,在Address后面加上当前客户的CustomerName信息
        /// </summary>
        /// <param name="customers">待更新的客户信息</param>
        protected abstract void UpdateCustomers(IEnumerable<Customer> customers);

        /// <summary>
        ///     批量删除指定编号的客户信息
        /// </summary>
        /// <param name="customerIds">待删除的客户编号集合</param>
        protected abstract void DeleteCustomers(IEnumerable<int> customerIds);

        #endregion

        #region 复杂查询

        /// <summary>
        ///     查询所有已完成(Finished == true)的订单,构建订单视图模型
        /// </summary>
        /// <returns>满足条件的订单视图模型集合</returns>
        protected abstract IEnumerable<OrderView> RetrieveOrderViews();

        #endregion

        #endregion

        #region 私有方法

        /// <summary>
        ///     单个实体操作测试
        /// </summary>
        /// <returns>是否继续</returns>
        private bool SingleCrudTest()
        {
            string token = DateTime.Now.ToString("hhmmssfff");
            Stopwatch watch = new Stopwatch();
            Customer customer = new Customer
            {
                CustomerName = "郭明锋@中国" + token,
                ContactName = "郭明锋",
                Address = "北京,北京",
                PostalCode = "100001",
                Tel = "13800138001"
            };

            //单个客户添加
            watch.Restart();
            CreateCustomer(customer);
            watch.Stop();
            Console.WriteLine("单个实体添加成功,耗时:{0}", watch.Elapsed);

            //查询最后添加的客户信息
            watch.Restart();
            Customer lastCustomer = RetrieveCustomer(customer.CustomerName);
            watch.Stop();
            if (lastCustomer != null && lastCustomer.CustomerName == customer.CustomerName)
            {
                Console.WriteLine("单个实体查询成功,耗时:{0}", watch.Elapsed);
            }
            else
            {
                Console.WriteLine("单个实体查询失败,耗时:{0},测试终止。", watch.Elapsed);
                return false;
            }

            //更新上一步查询出来的客户信息
            watch.Restart();
            UpdateCustomer(lastCustomer);
            watch.Stop();
            lastCustomer = RetrieveCustomer(customer.CustomerName);
            if (lastCustomer != null && lastCustomer.PostalCode == "100000" && lastCustomer.Tel == "13800138000")
            {
                Console.WriteLine("单个实体更新成功,耗时:{0}", watch.Elapsed);
            }
            else
            {
                Console.WriteLine("单个实体更新失败,耗时:{0},测试终止。", watch.Elapsed);
                return false;
            }

            //删除本次添加的单个客户信息
            watch.Restart();
            DeleteCustomer(lastCustomer.CustomerId);
            watch.Stop();
            lastCustomer = RetrieveCustomer(customer.CustomerName);
            if (lastCustomer == null)
            {
                Console.WriteLine("单个实体删除成功,耗时:{0}", watch.Elapsed);
            }
            else
            {
                Console.WriteLine("单个实体删除成功,耗时:{0},测试终止", watch.Elapsed);
                return false;
            }
            return true;
        }

        /// <summary>
        ///     批量实体操作测试
        /// </summary>
        /// <returns>是否继续</returns>
        private bool MultipleCrudTest()
        {
            string token = DateTime.Now.ToString("hhmmssfff");
            Stopwatch watch = new Stopwatch();
            Console.WriteLine("要开始批量测试,请输入批量大小:");
            int count;
            bool flag = int.TryParse(Console.ReadLine(), out count);
            while (!flag)
            {
                Console.WriteLine("要开始批量测试,请输入批量大小:");
                flag = int.TryParse(Console.ReadLine(), out count);
            }
            List<Customer> customers = new List<Customer>();
            for (int index = 0; index < count; index++)
            {
                customers.Add(new Customer
                {
                    CustomerName = string.Format("郭明锋@中国{0}{1}", token, index),
                    ContactName = "郭明锋",
                    Address = "北京,北京",
                    PostalCode = "100001",
                    Tel = "13800138001"
                });
            }
            Console.WriteLine("开始进行批量测试,测试数量为:{0}", count);

            //批量客户添加
            watch.Restart();
            CreateCustomers(customers);
            watch.Stop();
            Console.WriteLine("批量实体添加成功,耗时:{0}", watch.Elapsed);

            //查询最后添加的客户信息
            watch.Restart();
            List<Customer> lastCustomers = RetrieveCustomers(count).ToList();
            watch.Stop();
            //以前后客户名称集合取差集来判断是否一致
            if (!lastCustomers.Select(m => m.CustomerName).Except(customers.Select(m => m.CustomerName)).Any())
            {
                Console.WriteLine("批量实体查询成功,耗时:{0}", watch.Elapsed);
            }
            else
            {
                Console.WriteLine("批量实体查询失败,耗时:{0},测试终止。", watch.Elapsed);
                return false;
            }

            //批量更新客户信息
            watch.Restart();
            UpdateCustomers(lastCustomers);
            lastCustomers = RetrieveCustomers(count).ToList();
            if (!lastCustomers.Select(m => m.Address).Intersect(customers.Select(m => m.Address)).Any())
            {
                Console.WriteLine("批量实体更新成功,耗时:{0}", watch.Elapsed);
            }
            else
            {
                Console.WriteLine("批量实体更新失败,耗时:{0},测试终止。", watch.Elapsed);
                return false;
            }

            //批量删除本次添加的客户信息
            List<int> customerIds = lastCustomers.Select(m => m.CustomerId).ToList();
            watch.Restart();
            DeleteCustomers(customerIds);
            watch.Stop();
            lastCustomers = RetrieveCustomers(count).ToList();
            if (!lastCustomers.Select(m => m.CustomerName).Intersect(customers.Select(m => m.CustomerName)).Any())
            {
                Console.WriteLine("批量实体删除成功,耗时:{0}", watch.Elapsed);
            }
            else
            {
                Console.WriteLine("批量实体删除失败,耗时:{0},测试终止。", watch.Elapsed);
                return false;
            }
            return true;
        }

        private void RetrieveComplex()
        {
            Stopwatch watch = new Stopwatch();
            watch.Restart();
            List<OrderView> orderViews = RetrieveOrderViews().ToList();
            watch.Stop();
            Console.WriteLine("复杂查询执行成功,耗时{1},共获取{0}个订单视图信息。", watch.Elapsed, orderViews.Count);
        }

        #endregion

        #region 公共方法

        /// <summary>
        ///     开始测试工作,主要对 Customer 表进行操作,工作顺序为增、查、改、删,进行单个操作,批量操作与复杂查询
        /// </summary>
        public void Work()
        {
            bool isContinue = SingleCrudTest();
            if (!isContinue)
            {
                return;
            }
            isContinue = MultipleCrudTest();
            if (!isContinue)
            {
                return;
            }
            RetrieveComplex();
        }

        #endregion
    }
}

四、如何使用(以EntityFramework为例)

下面,我就以EntityFramework 4.4 来演示一下怎样使用这个测试框架。可能有同学会说,EF6都出来了,为什么还使用4.4版本?原因一、.net4.0只支持到4.4版本,原因二、windows 2003 只支持到 .net 4.0,原因三、我正在使用的是4.4版本。如果你觉得4.4版本 out 了,可以自己去实现一个 EF6的测试示例,呵呵。废话不多说,下面我们来实现 EntityFramework 4.4 的测试示例。

(一) 在项目中建立专属文件夹

为了更好的管理各个数据访问方案的实现代码,也为防止项目结构混乱,各个方案的代码应在自己的文件夹内实现。在这里,创建一个名为EntityFramework的文件夹

(二) 在文件夹内实现数据操作的基础准备

首先要进行相应数据访问方案的基础准备,比如ado.net方案,可能需要一个SqlHelper的辅助操作类,又或者其他ORM,需要进行数据映射,生成数据实体等等。对于EntityFramework,只需要实现一个数据上下文类即可:

namespace DataTestFramework.EntityFramework
{
    public class EFDbContext : DbContext
    {
        public EFDbContext(string connectionStringOrName)
            : base(connectionStringOrName) { }

        public DbSet<Customer> Customers { get; set; }

        public DbSet<Category> Categories { get; set; }

        public DbSet<Product> Products { get; set; }

        public DbSet<Order> Orders { get; set; }

        public DbSet<OrderDetail> OrderDetails { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Product>().HasRequired(m => m.Category).WithMany(n => n.Products).HasForeignKey(m => m.CategoryId);
            modelBuilder.Entity<Order>().HasRequired(m => m.Customer).WithMany(n => n.Orders).HasForeignKey(m => m.CustomerId);
            modelBuilder.Entity<OrderDetail>().HasRequired(m => m.Product).WithMany(n => n.OrderDetails).HasForeignKey(m => m.ProductId);
            modelBuilder.Entity<OrderDetail>().HasRequired(m => m.Order).WithMany(n => n.OrderDetails).HasForeignKey(m => m.OrderId);
        }
    }
}

特别说明:项目中已经添加了测试所需的POCO实体类,如果这些类不符合具体的数据访问方案的要求(比如实体类是代码生成器来生成的),可以自行在自己的文件夹中定义需要的实体类,在数据访问实现中再把操作结果转换为系统定义的POCO实体类,以进行数据操作结果的验证。

(三) 继承TesterBase实现自己的Tester类

测试基类 TesterBase 中已定义了需要实现的测试用例,需要继承这个基类,实现相应数据访问方案的具体操作实现。对于 EntityFramework,实现如下:

namespace DataTestFramework.EntityFramework
{
    /// <summary>
    ///     EntityFramework测试类
    /// </summary>
    public class EntityFrameworkTester : TesterBase
    {
        private const int _pageSize = 300;

        #region 单个操作

        /// <summary>
        ///     添加单个客户信息
        /// </summary>
        /// <param name="customer">待添加的客户信息</param>
        protected override void CreateCustomer(Customer customer)
        {
            using (EFDbContext db = new EFDbContext(ConnectionString))
            {
                db.Customers.Add(customer);
                db.SaveChanges();
            }
        }

        /// <summary>
        ///     获取指定名称的客户信息
        /// </summary>
        /// <param name="customerName">带Token的客户名称</param>
        /// <returns>指定名称的客户信息,不存在时返回null</returns>
        protected override Customer RetrieveCustomer(string customerName)
        {
            using (EFDbContext db = new EFDbContext(ConnectionString))
            {
                return db.Customers.SingleOrDefault(m => m.CustomerName == customerName);
            }
        }

        /// <summary>
        ///     更新指定客户信息的 PostalCode="100000",Tel="13800138000"
        /// </summary>
        /// <param name="customer">待更新的客户信息</param>
        protected override void UpdateCustomer(Customer customer)
        {
            customer.PostalCode = "100000";
            customer.Tel = "13800138000";
            using (EFDbContext db = new EFDbContext(ConnectionString))
            {
                if (db.Entry(customer).State == EntityState.Detached)
                {
                    db.Customers.Attach(customer);
                    db.Entry(customer).State = EntityState.Modified;
                }
                db.SaveChanges();
            }
        }

        /// <summary>
        ///     删除指定客户信息
        /// </summary>
        /// <param name="customerId">客户编号</param>
        protected override void DeleteCustomer(int customerId)
        {
            using (EFDbContext db = new EFDbContext(ConnectionString))
            {
                Customer customer = db.Customers.SingleOrDefault(m => m.CustomerId == customerId);
                if (customer == null)
                {
                    return;
                }
                db.Customers.Remove(customer);
                db.SaveChanges();
            }
        }

        #endregion

        #region 批量操作

        /// <summary>
        ///     批量添加客户信息
        /// </summary>
        /// <param name="customers">待添加的客户信息集合</param>
        protected override void CreateCustomers(IEnumerable<Customer> customers)
        {
            using (EFDbContext db = new EFDbContext(ConnectionString))
            {
                try
                {
                    db.Configuration.AutoDetectChangesEnabled = false;
                    List<Customer> customerList = customers as List<Customer> ?? customers.ToList();
                    int pageCount = customerList.Count / _pageSize;
                    pageCount = customerList.Count % _pageSize > 0 ? pageCount + 1 : pageCount;
                    for (int index = 0; index < pageCount; index++)
                    {
                        List<Customer> pageData = customerList.Skip(index * _pageSize).Take(_pageSize).ToList();
                        foreach (Customer customer in pageData)
                        {
                            db.Customers.Add(customer);
                        }
                        db.SaveChanges();
                    }
                }
                finally
                {
                    db.Configuration.AutoDetectChangesEnabled = true;
                }
            }
        }

        /// <summary>
        ///     反向(先倒序排序)获取指定数量的客户信息
        /// </summary>
        /// <param name="count">要获取的数量</param>
        /// <returns>获取的客户信息集合</returns>
        protected override IEnumerable<Customer> RetrieveCustomers(int count)
        {
            using (EFDbContext db = new EFDbContext(ConnectionString))
            {
                return db.Customers.OrderByDescending(m => m.CustomerId).Take(count).ToList();
            }
        }

        /// <summary>
        ///     更新指定的多个客户信息,在Address后面加上当前客户的CustomerName信息
        /// </summary>
        /// <param name="customers">待更新的客户信息</param>
        protected override void UpdateCustomers(IEnumerable<Customer> customers)
        {
            using (EFDbContext db = new EFDbContext(ConnectionString))
            {
                List<int> ids = customers.Select(m => m.CustomerId).ToList();
                int pageCount = ids.Count / _pageSize;
                pageCount = ids.Count % _pageSize > 0 ? pageCount + 1 : pageCount;
                for (int index = 0; index < pageCount; index++)
                {
                    List<int> pageIds = ids.Skip(index * _pageSize).Take(_pageSize).ToList();
                    db.Customers.Update(m => pageIds.Contains(m.CustomerId), n => new Customer
                    {
                        Address = n.Address + n.CustomerName
                    });
                }
            }
        }

        /// <summary>
        ///     批量删除指定编号的客户信息
        /// </summary>
        /// <param name="customerIds">待删除的客户编号集合</param>
        protected override void DeleteCustomers(IEnumerable<int> customerIds)
        {
            List<int> ids = customerIds as List<int> ?? customerIds.ToList();
            using (EFDbContext db = new EFDbContext(ConnectionString))
            {
                int pageCount = ids.Count / _pageSize;
                pageCount = ids.Count % _pageSize > 0 ? pageCount + 1 : pageCount;
                for (int index = 0; index < pageCount; index++)
                {
                    List<int> pageIds = ids.Skip(index * _pageSize).Take(_pageSize).ToList();
                    db.Customers.Delete(m => pageIds.Contains(m.CustomerId));
                }
            }
        }

        #endregion

        #region 复杂查询

        /// <summary>
        ///     查询所有已完成(Finished == true)的订单,构建订单视图模型
        /// </summary>
        /// <returns>满足条件的订单视图模型集合</returns>
        protected override IEnumerable<OrderView> RetrieveOrderViews()
        {
            using (EFDbContext db = new EFDbContext(ConnectionString))
            {
                var orders = db.Orders.Where(m => m.Finished).Select(m => new
                {
                    m.OrderId,
                    m.OrderDate,
                    m.SumMoney,
                    m.Finished,
                    Customer = new { m.Customer.CustomerName },
                    ProductNames = m.OrderDetails.Select(n => n.Product.ProductName)
                }).ToList();
                return orders.Select(order => new OrderView
                {
                    OrderId = order.OrderId,
                    OrderDate = order.OrderDate,
                    SumMoney = order.SumMoney,
                    Finished = order.Finished,
                    CustomerName = order.Customer.CustomerName,
                    ProductNames = order.ProductNames.ExpandAndToString(",")
                }).ToList();
            }
        }

        #endregion
    }
}

对于批量修改,删除操作,我使用了 EntityFramework.Extended.dll 程序集(可以在Nuget中获取)来进行实现,该程序集对于每次批量操作只生成一条sql语句,而不会像EntityFramework提供的原生方法那样批量N条数据就要生成N条sql语句。并且使用了逐部分处理的方式,以免进行大量数据处理时一次提交太多的数据导致性能低下。

复杂查询的业务,对于EntityFramework来说,实现是相当简单的,完全是要什么取什么,使用IQueryable<T>的扩展方法Select来按需获取,然后用匿名类来装载查询结果,再根据这个查询结果构造视图模型的列表集合,具体实现如上面代码中 166行-197行所示。

添加EntityFrameworkTester类之后的 Code Map如下所示:

(四) 添加测试调用入口

在Program类中,定义了很多的Method方法供调用,请不要修改Program类的Main方法,只需要在 HelpInfo 方法中添加使用哪个序号的Method进行调用的信息,然后在相应序号的Method方法中,如这里EntityFramework使用 Method01方法来作为入口,则进行如下修改:

  1. 修改HelpInfo方法,添加第6行所示代码,其中1表示Method01的序号,后面为测试名称: 1 private static void HelpInfo() 2 { 3 Console.WriteLine("=============帮-助-信-息============"); 4 Console.WriteLine("h.帮助信息"); 5 Console.WriteLine("0.退出程序"); 6 Console.WriteLine("1.性能测试——EF"); 7 Console.WriteLine("=============帮-助-信-息============"); 8 }

  2. 修改Method01方法,实例化Tester类,调用Work方法 1 private static void Method01() 2 { 3 Console.WriteLine("=============EntityFramework测试开始============"); 4 EntityFrameworkTester tester = new EntityFrameworkTester(); 5 tester.Work(); 6 Console.WriteLine("=============EntityFramework测试结束============"); 7 }

(五) 运行测试

至此,EntityFramework使用此测试框架的代码已添加完毕,运行测试,忽略第一次运行的结果,分别执行5000条与10000条的批量数据操作,结果如下:

五、写在后面

现在代码示例中只有本人添加的一个EntityFramework示例,其他的方案比如ado.net及其他ORM期待高手来添加,请大家下载并编写自己的数据访问方案的实现,可在评论处留下载链接,也可直接发给我,我整理之后将在本系列后续中详解实现过程,并进行综合的对比。

另外,如果当前框架不能满足部分数据访问方案的要求导致无法添加测试的话,也希望能提出来,我再进行更新。

六、源码下载

本框架的代码已发布到 codeplex 上,同学们可以通过VS自带的团队项目管理功能(TFS)或SVN 随时获取最新代码。

  • 项目地址:https://datatester.codeplex.com

  • TFS获取地址:https://tfs.codeplex.com:443/tfs/TFS17,(似乎只有团队成员可以用TFS方式)需要登录,并在项目的 SOURCE CODE 标签的 Connect 连接处获取VS端的登录用户名,密码为自己的账号密码。

  • SVN获取地址:https://datatester.svn.codeplex.com/svn

  • 以上方案都不可用的情况下,可以项目的 SOURCE CODE 标签页选择直接下载最新的源码压缩包