65.9K
CodeProject 正在变化。 阅读更多。
Home

Entity Framework 5.0 通用存储库类,用于带复杂 LINQ 表达式的 CRUD 操作。Microsoft Visual Studio Test Tools 单元测试

2015 年 4 月 3 日

CPOL

9分钟阅读

viewsIcon

30471

downloadIcon

750

本文演示了如何使用 Entity Framework 5.0 数据库优先方法,开发不仅用于 CRUD 操作,还用于使用 System.Linq.Expressions.Expression 参数作为谓词进行搜索的通用类。正如我在上一篇文章中已经演示过的代码优先方法。它也

引言

在大多数 Web 应用程序中,常见的用户交互总是使用不同的谓词参数来添加/编辑/删除和搜索信息。通用类可以在大规模开发中重用,因为它需要最少的开发者工作量,而不是从头开始编写每一段代码。最好在处理用户端的信息之前和之后抽象出所需的一段代码。

例如。如果我需要搜索应用程序中的任何页面上的任何信息,我需要发送搜索参数,读取搜索参数并将其提供给实际的搜索查询(无论是 linq 还是存储过程),然后获取结果并进行处理,以便仅以 JSON 或 XML 格式获取所需信息,而不是将所有信息发送回网页。

简而言之,无论你是想泡茶还是泡咖啡,都有一些步骤是两者都通用的。通用部分可以实现通用化,这样开发人员就不必为每个页面/屏幕一遍又一遍地编写它。

背景

我遇到了一种情况,我的大部分屏幕都有相同的功能,例如,每当用户导航到任何菜单时,它总是显示网格中的现有信息,并为最终用户提供单个面板中的所有 CRUD 操作。在这里,我开始思考除了抽象数据访问层之外,哪些部分可以抽象到单独的层中。

为了充分利用模式、实践和面向对象概念的优势,我开始设计通用类,这确实大大减少了我所有开发人员的工作量,即使屏幕像用户主屏幕一样简单,或者是一个非常复杂的请求表单,其中包含大量去规范化的数据,并以 Bootstrap 或 jQuery tabs 的形式呈现。

第一个通用类用于与 Web 页面交互。例如,在 MVC 示例中,HTML 视图应始终与通用的 BaseController 交互,并处理所需的请求,但需要由开发人员的控制器覆盖的特定信息除外。

第二个通用类用于与数据访问层交互。同样,在处理来自数据访问层的信息时,开发人员可能会得到一个简单的实体(如果是主屏幕)或一个来自复杂屏幕的复杂实体模型。所以我决定采用两种类型的通用存储库类:一种用于简单的主屏幕 CRUD 操作,另一种用于复杂的模型实体类。这样,开发人员在执行 CRUD 操作时就不必从复杂模型中提取特定实体所需的信息,而是由存储库类使用自定义反射来处理,并将所需实体从复杂实体传递到数据库以执行 CRUD 操作。

在本文中,我将只演示数据访问层部分。下一篇文章将是一个 MVC 示例,其中我们将有一个通用的通用基控制器。

用户可能需要一个简单的通用存储库类来通过传递简单的实体名称(数据库表名)来添加、更新或删除记录(带或不带参数),或者他可能有一个复杂的模型类,它是多个实体的组合(例如,带有部门信息或总订单等的客户类,因为它通过表单集合从屏幕获取)。

引用

由于上传大小限制,我删除了 bin 文件夹和 packages 中的所有 dll。下载源代码后,如果您的 visual studio 中不存在,您需要使用 NUGate 包管理器下载 dll,或者在第一次重建解决方案时会自动下载

使用代码

首先,我们需要创建一个新的 ASP.NET MVC 4 Web Application 项目,并选择 Internet 应用程序作为项目模板。不要忘记包含 Unit test Project,因为我们将使用单元测试类来测试所有这些通用类和方法。将解决方案命名为 DemoGenericClasses. 构建并运行解决方案,它应该可以顺利编译和运行。再添加一个类库项目,并将其命名为 DataModel。删除默认的 class-1 文件,然后开始按照步骤操作

步骤 1 添加新项 ADO.NET Entity Data Model ,将其命名为 DemoEntities.edmx 。选择从数据库生成选项并创建新连接,然后按照我在源代码--> snapshots 文件夹中提供的步骤进行操作。感谢 Microsoft,Entity Framework 5 的实体类不再继承自任何 Entity Object 类。现在您可以自由地更改实体类,因为它们是简单的独立部分类。 

步骤 2 在数据模型层创建四个文件夹,以实现更结构化的开发方式。它们分别是 Model Entities、Interfaces、Helper Classes 和 Repository Classes。每个文件夹将包含其名称所示的某些类和接口。

步骤 3 添加我们将用于各种地方的 Helper Classes,以在设计类和逻辑块时遵循特定原则。

.

引用

 请注意,当您添加一个类时,Microsoft 默认会在您的类中添加许多 Using 程序集,这些程序集对于您在该类中编写的逻辑代码块没有用处。最佳实践是删除未使用的 Using,方法是右键单击您的类--> Organize Usings--> Remove Unused Usings

IndexedListItem 用于维护主键,尤其是在添加操作后,如果您需要使用标识列值更新现有实体。同时用于维护一个虚拟索引,以基于临时集合维护序列号,该集合可能在网格中的 CRUD 操作期间使用。

namespace DataModel.HelperClasses
{
    public class IndexedListItem
    {
        public int MyIndex { get; set; }

        public int PrimaryKey { get; set; }
    }
}

PredicateBuilder  用于准备使用 System.Linq.Expressions.Expression 的谓词,这在基于用户输入过滤实体集合时非常有用。

clsEntityBaseClass   类,它将包含一些公共属性,例如,当您不使用默认实体而是想使用复杂视图模型时

using System;

namespace DataModel.HelperClasses
{
    public class clsEntityBaseClass : IndexedListItem
    {
        public string CreatedBy { get; set; }

        public DateTime? CreatedDate { get; set; }

        public string ModifiedBy { get; set; }

        public DateTime? ModifiedDate { get; set; }

        public decimal? SortOrder { get; set; }
    }
}

要添加其他属性和逻辑,我们可以从 clsEntityBaseClass 继承 DemoEntities.tt 中生成的局部类,并将 T 限制为特定类型而不是仅为 Type 类。

简单的 Customer 实体类,在 DemoEntities.tt 中继承自 clsEntityBaseClass 

namespace DataModel
{
    using System;
    using System.Collections.Generic;
     // inherited the autogenerated class with our own custom class clsEntityBaseclass
    public partial class Customer:clsEntityBaseClass
    {
        public Customer()
        {
            this.Sales = new HashSet<Sale>();
        }
    
        public int CustomerID { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string Zip { get; set; }
        public Nullable<System.DateTime> Timestamp { get; set; }
        public int DepartmentId { get; set; }
        public virtual ICollection<Sale> Sales { get; set; }
        public virtual Department Department { get; set; }
    }
}

步骤 3 创建一个复杂的 Customer Model 类,它将与 Customer 实体类不同,如下所示

using DataModel.HelperClasses;
using System;
namespace DataModel
{
    public class clsCustomerModel : clsEntityBaseClass, IAggregateRoot
    {
        public int DepartmentId { get; set; }
        public int CustomerID { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string Zip { get; set; }
        public Nullable<System.DateTime> Timestamp { get; set; }

        //Addtional attributes which are not part of customer class or table
        public int TotalOrders { get; set; }
        public string OverallRating { get; set; }

        public clsEntityBaseClass GetTableName
        {
            get { return new Customer(); }
        }

        public string EntitySetName
        {
            get { return "Customers"; }
        }
    }
}

步骤 4 现在我们已经准备好了所有类型的实体。在这里,我们将有两个不同的通用类:一个用于简单的 Customer 类型实体,另一个用于复杂的实体,如 Customer Model 类。在这两者中,我们将拥有相同的类方法,如 Add、Update、Delete、FindAll、FindById 和 SearchByparam,并为 Add 和 Delete 等提供重载方法。

我们倾向于进行面向接口的编程,因此,除非方法向外部世界公开,否则我们的大部分方法都可以无需接口进行设计。

步骤 5 创建两个接口。我们将在两个通用类中使用略有不同的方法名,以便在单元测试期间更容易理解。

IGenericRepository 是一个接口,它将 T 作为简单实体类类型的参数,例如,Customer 类。我们将在 GenericRepository 类中实现此接口

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace DataModel
{
    public interface IGenericRepository<T> where T : class
    {
        IEnumerable<T> GetAll();
        T GetByID(object id);
        IEnumerable<T> FilterByParam(Expression<Func<T, bool>> predicate);
        void Insert(T obj);    
        void Update(T obj);    
        void Delete(object id);    
        void Save();
    }
}

IGenericComplexRepository 是第二个接口,它将有两个泛型参数 T 和 M,其中

T 是任何继承自 IndexedListItem 并实现了 IAggregateRoot 接口的类,我们将使用它来从要添加、更新或删除的复杂模型中获取实体类的名称。至少带有一个无参数的构造函数

M 再次是任何继承自基类 clsEntityBaseClass 的类

using DataModel.HelperClasses;
using System.Collections.Generic;

namespace DataModel
{
    public interface IGenericComplexRepository<T, M>
        where T : IndexedListItem, IAggregateRoot, new()
        where M : clsEntityBaseClass
    {
        void Add(List<T> entityList);
        void Update(T entity);
        void Remove(T entity);
        void RemoveById(int Id);
        List<T> FindAll();
        T FindById(int id);

    }
}

现在我们几乎准备好使用适当的通用类型参数来实现这些通用接口了。

步骤 6 创建两个通用存储库类,分别命名为 GenericComplexRepository 和 GenericRepository,它们分别实现步骤 5 中创建的接口。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data.Entity;
using System.Data;
using DataModel.HelperClasses;
using System.Data.Entity.Validation;

namespace DataModel
{
    public abstract class GenericRepository<T> : IGenericRepository<T> where T : IndexedListItem, new()
    {
        internal DemoTestEntities objDemoTestEntities;
        internal DbSet<T> dbSet;

        public GenericRepository()
        {
            this.objDemoTestEntities = new DemoTestEntities();
            this.dbSet = objDemoTestEntities.Set<T>();
        }

        public IEnumerable<T> GetAll()
        {
            IQueryable<T> objT = dbSet;
            return objT.ToList();
        }

        public T GetByID(object id)
        {
            return dbSet.Find(id);
        }

        public IEnumerable<T> FilterByParam(System.Linq.Expressions.Expression<Func<T, bool>> predicate)
        {
            return dbSet.Where(predicate).ToList();
        }

        public void Insert(T obj)
        {
            dbSet.Add(obj);
        }

        public void Update(T obj)
        {
            dbSet.Attach(obj);
            objDemoTestEntities.Entry(obj).State = EntityState.Modified;
        }

        public void Delete(object id)
        {
            T ObjT = dbSet.Find(id);
            if (ObjT != null)
            {
                Delete(ObjT);
            }

        }

        public virtual void Delete(T obj)
        {
            if (objDemoTestEntities.Entry(obj).State == EntityState.Detached)
            {
                dbSet.Attach(obj);
            }
            dbSet.Remove(obj);
        }

        public void Save()
        {
            try
            {
                objDemoTestEntities.SaveChanges();
            }

            catch (DbEntityValidationException e)
            {
                foreach (var eve in e.EntityValidationErrors)
                {
                    Console.WriteLine("Entity of type \"{0}\" in state \"{1}\" has the following validation errors:",
                        eve.Entry.Entity.GetType().Name, eve.Entry.State);
                    foreach (var ve in eve.ValidationErrors)
                    {
                        Console.WriteLine("- Property: \"{0}\", Error: \"{1}\"",
                            ve.PropertyName, ve.ErrorMessage);
                    }
                }
                throw;
            }
            catch (Exception ex)
            {
                throw ex;
            }

        }

        public void Dispose()
        {
            if (objDemoTestEntities != null)
            {
                objDemoTestEntities.Dispose();
                objDemoTestEntities = null;
            }
        }

    }
}

 

对于 GenericComplexRepository,我这里不展示它,因为它有完整的注释且篇幅很长,因为它使用反射来提取实体列并将其放入映射到数据库表的简单实体类中以对其进行 CRUD 操作。现在我们已经准备好了通用类。现在是开发人员尽可能多地重用这些类来执行不同实体上的不同操作的时候了。

步骤 7 创建三个不同的开发人员存储库类,它们将根据开发人员的需要继承自通用类。开发人员可以拥有自己的方法,以及从通用类中获得的通用 Add、Update、Delete、FindAll 和 Searchbyparam 方法。对于大多数逻辑块,开发人员不必编写任何代码。

CustomerRepository 类表示 通用存储库类 将与 Customer 实体相关联,用于对数据库中的 Customer 表执行 CRUD 操作。

namespace DataModel
{
    public class CustomerRepository : GenericRepository<Customer>
    {
    }
}

DepartmentRepository 类表示 通用存储库类 将与 Department 实体相关联,用于对数据库中的 Department 表执行 CRUD 操作。

namespace DataModel

{
    public class DepartmentRepository : GenericRepository<Department>
    {
    }
}

clsCustomerModelRepository 类表示 通用复杂存储库类 将与 clsCustomerModel 实体相关联,用于对数据库中的 Customer 表执行 CRUD 操作。

namespace DataModel
{
    public class clsCustomerModelRepository : GenericComplexRepository<clsCustomerModel, Customer>
    {
    }
}

步骤 8 是时候测试通用类中可用的这些方法了。在 DemoGenerics.Tests 项目中为相应的开发人员存储库类创建单元测试类。在添加新的单元测试类之前,不要忘记在那里添加 DataModel 层的引用。clsCustomerModelTest、CustomerRepoTest、DepartmentRepoTest,其中一个代码片段是

引用

请注意,这里我将实际结果推送到预期结果以获得 Assert.AreEqual(expected, actual); 为 true。理想情况下,我们应该在 expected 对象中有硬编码值,而 actual 将是目标方法返回的结果。

using System;
using DataModel;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace DemoGenerics.Tests
{
    [TestClass]
    public class CustomerRepoTest
    {
        private TestContext testContextInstance;

        /// <summary>
        ///Gets or sets the test context which provides
        ///information about and functionality for the current test run.
        ///</summary>
        public TestContext TestContext
        {
            get
            {
                return testContextInstance;
            }
            set
            {
                testContextInstance = value;
            }
        }

        [TestMethod()]
        [DeploymentItem("DataModel.dll")]
        public void GetAllCustomerTest()
        {
            CustomerRepository target = new CustomerRepository();
            List<Customer> expected = new List<Customer>();
            List<Customer> actual;

            actual = (List<Customer>)target.GetAll();
            expected = actual;
            Assert.AreEqual(expected, actual);
        }

        [TestMethod()]
        [DeploymentItem("DataModel.dll")]
        public void GetbyIdCustomerTest()
        {
            CustomerRepository target = new CustomerRepository();
            Customer expected = new Customer();
            Customer actual;
            object CustomerId = 1;

            actual = (Customer)target.GetByID(CustomerId);
            expected = actual;
            Assert.AreEqual(expected, actual);
        }

        [TestMethod()]
        [DeploymentItem("DataModel.dll")]
        public void FilterByParamCustomerTest()
        {
            //Prepare Search Criteria using Predicate Operators
            ParameterExpression d = Expression.Parameter(typeof(Customer), "cust");
            Expression pdFirstName = Expression.Equal(Expression.Property(d, "FirstName"), Expression.Constant("Murtuza"));
            Expression pdDepartmentId = Expression.Equal(Expression.Property(d, "DepartmentId"), Expression.Constant(2));
            Expression<Func<Customer, bool>> pdfinalPredicate = Expression.Lambda<Func<Customer, bool>>(
                Expression.Or(pdFirstName, pdDepartmentId), d);

            //Prepare Search Criteria using single Lambda Expression
            Expression<Func<Customer, bool>> pdfinalexpression = (u) => (u.FirstName.Contains("M") || u.DepartmentId.Equals(2));

            CustomerRepository target = new CustomerRepository();
            List<Customer> expected = new List<Customer>();
            List<Customer> actual;

            actual = (List<Customer>)target.FilterByParam(pdfinalPredicate);
            expected = actual;
            Assert.AreEqual(expected, actual);
        }

        [TestMethod()]
        [DeploymentItem("DataModel.dll")]
        public void InsertCustomerTest()
        {
            CustomerRepository target = new CustomerRepository();
            Customer expected = new Customer();
            Customer actual = new Customer();
            target.Insert(new Customer { FirstName = "Murtuza", LastName = "Patel", City = "Navi Mumbai", State = "MS", Zip = "410208", DepartmentId = 1 });
            target.Save();
            expected = actual;
            Assert.AreEqual(expected, actual);
        }

        [TestMethod()]
        [DeploymentItem("DataModel.dll")]
        public void UpdateCustomerTest()
        {
            CustomerRepository target = new CustomerRepository();
            Customer expected = new Customer();
            Customer actual = new Customer();

            target.Update(new Customer { CustomerID = 1, FirstName = "Murtuza", LastName = "Patel", City = "Navi Mumbai", State = "MS", Zip = "410208", DepartmentId = 3, Timestamp = Convert.ToDateTime("02-04-2015") });
            target.Save();
            expected = actual;
            Assert.AreEqual(expected, actual);
        }

        [TestMethod()]
        [DeploymentItem("DataModel.dll")]
        public void DeleteCustomerTest()
        {
            CustomerRepository target = new CustomerRepository();
            Customer expected = new Customer();
            Customer actual = new Customer();
            object CustomerId = 20;

            target.Delete(CustomerId);
            target.Save();
            expected = actual;
            Assert.AreEqual(expected, actual);
        }

    }
}

要准备谓词,有许多方法可以准备谓词以将其传递给通用方法,以便根据搜索条件搜索实体。我添加了两种方法,甚至添加了一个 PredicateBuilder 类来实现相同的目的。

关注点

最后,我实现了一些东西,获得了开发人员的赞许,他们说我在数据访问层上没有编写任何代码就开发了我的屏幕,至少在执行 CRUD 操作方面是如此。Microsoft 的单元测试是一个很好的工具,可以在不准备任何 HTML 元素的情况下测试您的逻辑块。单元测试是另一个好处,可以确保通用类中编写的所有方法都通过了,并且在为相应的开发人员存储库类设计 HTML 视图后能够正常工作。

历史

将继续撰写此类通用控制器类,以与不同的 HTML 视图进行交互并将数据传递给业务层。

© . All rights reserved.