Thứ Năm, 6 tháng 11, 2014

Generic Repository and Unit of Work Patterns with Service layer in an ASP.NET MVC. Ninject, MS Unit Test, Moq

1. Implementing Generic Repository

You need to create IGenericRepository and GenericRepository as shown bellow
//IGenericRepository.cs
public interface IGenericRepository where T : class
{
        List GetAll();
        List FindBy(Expression> predicate);
        bool Add(T entity);
        bool Delete(T entity);
        bool Edit(T entity);
        T FindById(int id);
}

//GenericRepository.cs
public  class GenericRepository : IGenericRepository where T : class
{
        public GenericRepository(MyStoreContext context)
        {
            _entities = context;
        }

        private MyStoreContext _entities;
        public MyStoreContext db
        {

            get { return _entities; }
            set { _entities = value; }
        }

        public virtual List GetAll()
        {
            IQueryable query = _entities.Set();
            return query.ToList();
        }

        public List FindBy(System.Linq.Expressions.Expression> predicate)
        {
            IQueryable query = _entities.Set().Where(predicate);
            return query.ToList();
        }

        public virtual void Attach(T entity)
        {
            _entities.Set().Attach(entity);
        }

        public virtual bool Add(T entity)
        {
            _entities.Set().Add(entity);
            return true;
        }

        public virtual bool Delete(T entity)
        {
            _entities.Set().Remove(entity);
            return true;
        }

        public virtual bool Edit(T entity)
        {
            _entities.Entry(entity).State = EntityState.Modified;
            return true;
        }

        public virtual T FindById(int id)
        {
           return  _entities.Set().Find(id);
        }
}


2. Implementing Unit of Work

DbContext contains unit of work by default , if you make series of changes to entities and call SaveChanges method , either all the changes make will be persisted to the database or if any of the change is failed to be persisted then all changes will be rolled back to the state prior to calling savechanges.
We created the unit of work to make our transactional change to be testable at the service layer and to get some flexibility.

 //IUnitOfWork.cs
public interface IUnitOfWork:IDisposable
{
        IGenericRepository ProductRepository { get;  }
        IGenericRepository OrderRepository { get;  }
       void Save();
}

//UnitOfWork.cs
public class UnitOfWork : IUnitOfWork 
{
        private readonly MyStoreContext _context;
        private IGenericRepository productRepository;
        private IGenericRepository orderRepository;
      
        public UnitOfWork()
        {
            this._context = new MyStoreContext();
        }

        public IGenericRepository ProductRepository
        {
            get { return this.productRepository ?? (this.productRepository = new GenericRepository(_context)); }
        }

        public IGenericRepository OrderRepository
        {
            get { return this.orderRepository ?? (this.orderRepository = new GenericRepository(_context)); }
        }       

        public void Save()
        {            
            _context.SaveChanges();
        }

        private bool disposed = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    _context.Dispose();
                }
            }
            this.disposed = true;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
}

Here we have prepared our unit of work, if you want to make our unit of work to be testable you can factor our DbContext and inject to our unit of work. 

3. Introducing Service Layer


The service layers the one the controller is going to interact, this service layer can be used for various purposes, and the first one will be to implement CRUD operation.  Since service layer is consuming unit of work we can also make changes to the group of objects and call unit of work save method. The  will be transactional because unit of work will indirectly call to dbcontext savechanges melthod which of course is transactional.
NOTE: The service layer will be used for transactional execution only if  your transaction target only single dbcontext object , if you are targeting multiple dbcontext objects you need to address this issue independently and not covered in this article.

//IProductService.cs
public interface IProductService:IDisposable
{
       void AddProduct(Product product);
       void UpdateProduct(Product product);
       void DeleteProduct(int id);
       List GetAllProduct();
       Product GetProduct(int productId);
}

//ProductService.cs
public class ProductService:IProductService
{
       private readonly  IUnitOfWork _unitOfWork;
      
      public ProductService(IUnitOfWork unitOfWork)
       {
           this._unitOfWork = unitOfWork;
       }
   
       public void AddProduct(Product product)
       {
           _unitOfWork.ProductRepository.Add(product);
           _unitOfWork.Save();           
       }

       public void UpdateProduct(Product product)
       {
           _unitOfWork.ProductRepository.Edit(product);
           _unitOfWork.Save();
       }

       public  void DeleteProduct(int id)
       {
           var org = _unitOfWork.ProductRepository.FindById(id);
           _unitOfWork.ProductRepository.Delete(org);
           _unitOfWork.Save();
       }

       public List GetAllProduct()
       {
           return _unitOfWork.ProductRepository.GetAll();
       }

       public Product GetProduct(int productId)
       {
           return _unitOfWork.ProductRepository.FindById(productId);
       }

       public void Dispose()
       {
           _unitOfWork.Dispose();           
       }
}

4. Setting Up Ninject

Since this design favours isoloation of each layer , we need dependency resolver to instantiate and supply dependent objects from one layer to another, for this purpose I have used Ninject dependency resolver. 
1. You have to install Ninject in your MVC project from nugget package
http://expertester.wordpress.com/2011/08/18/how-to-add-ninject-to-your-project-using-visual-studio-2010-sp1/
2. Create Dependency Resolver Class  in your MVC project

//NinjectDependencyResolver.cs
public class NinjectDependencyResolver:IDependencyResolver
{
        private IKernel kernel;

        public NinjectDependencyResolver()
        {
            kernel = new StandardKernel();
            AddBindings();
        }
        public object GetService(Type serviceType)
        {
            return kernel.TryGet(serviceType);
        }

        public IEnumerable GetServices(Type serviceType)
        {
            return kernel.GetAll(serviceType);
        }

        private void AddBindings()
        {
            kernel.Bind().To().WhenInjectedInto();
            kernel.Bind().To();
        }
}

3. Register NinjectDependencyResolver in the global.asax 
// Global.asax.cs –Application_Start()
protected void Application_Start()
{
            AreaRegistration.RegisterAllAreas();

            DependencyResolver.SetResolver(new NinjectDependencyResolver());

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
}


5. Consuming Service Layer at the controller

The ProductController receives service layer as a constructor injection
//ProductController.cs
public class ProductController : Controller
{
        private readonly IProductService productService;
        public int PageSize = 4;
        //
        // GET: /Product/
      
        public ProductController(IProductService productServiceParam)
        {           
            this.productService = productServiceParam;
        }
      
        public ViewResult Index(int page=1)
        {
            ProductListViewModel model = new ProductListViewModel
            {
                        Products = productService.GetAllProduct()
                                       .OrderBy(p => p.ProductId)
                                       .Skip((page - 1)*PageSize)
                                       .Take(PageSize),
                                       PagingInfo = new PagingInfo
                                                                  {
                                                                      CurrentPage = page,
                                                                      ItemsPerPage = PageSize,
                                                                      TotalItems =
                                                                          productService.GetAllProduct().Count()
                                                                  }

             };

            return View(model);
        }

        //
        // GET: /Product/Details/5

        public ActionResult Details(int id = 0)
        {
            Product product = productService.GetProduct(id);
            if (product == null)
            {
                return HttpNotFound();
            }
            return View(product);
        }

        //
        // GET: /Product/Create

        public ActionResult Create()
        {
            return View();
        }

        //
        // POST: /Product/Create

        [HttpPost]
        public ActionResult Create(Product product)
        {
            if (ModelState.IsValid)
            {
               productService.AddProduct(product);
               
                return RedirectToAction("Index");
            }

            return View(product);
        }

        //
        // GET: /Product/Edit/5

        public ActionResult Edit(int id = 0)
        {
            Product product = productService.GetProduct(id);
            if (product == null)
            {
                return HttpNotFound();
            }
            return View(product);
        }

        //
        // POST: /Product/Edit/5

        [HttpPost]
        public ActionResult Edit(Product product)
        {
            if (ModelState.IsValid)
            {
                productService.UpdateProduct(product);
            
                return RedirectToAction("Index");
            }
            return View(product);
        }

        //
        // GET: /Product/Delete/5

        public ActionResult Delete(int id = 0)
        {
            Product product = productService.GetProduct(id);
            if (product == null)
            {
                return HttpNotFound();
            }
            return View(product);
        }

        //
        // POST: /Product/Delete/5
        [HttpPost, ActionName("Delete")]
        public ActionResult DeleteConfirmed(int id)
        {
            productService.DeleteProduct(id);
            return RedirectToAction("Index");
        }

        protected override void Dispose(bool disposing)
        {
            productService.Dispose();
            base.Dispose(disposing);
        }
 }


6. Implementing Test at the business 

In order to start writing unit test there are a couple of things you have to know , 
First as we have discussed earlier all the layers are separated nicely so you need something which simulates the DI task , on which we can configure all dependency required to run our test. For this purpose I will use Moq(Read more: https://code.google.com/p/moq/)
1. Install Moq in your test project
   https://code.google.com/p/moq/downloads/detail?name=Moq.4.0.10827.Final.zip&can=2&q=
   Extract and reference in your test project
2.Write your test class as shown below
//ProductServiceTests.cs

[TestClass]
    public class ProductServiceTest
    {
        [TestMethod]
        public void Can_Add_Product_Service()
        {
            //Arrange
            List products = new List()
                                         {
                                             new Product(){Description="P1",ProductId=1,ProductName="p1Name"},
                                             new Product(){Description="P2",ProductId=2,ProductName="p2Name"},
                                             new Product(){Description="P3",ProductId=3,ProductName="p3Name"},
                                             new Product(){Description="P4",ProductId=4,ProductName="p4Name"},
                                             new Product(){Description="P5",ProductId=5,ProductName="p5Name"},
                                             new Product(){Description="P6",ProductId=6,ProductName="p6Name"},

                                         };
            Mock> mock1 = new Mock>();

            //Here we are going to mock repository GetAll method 
            mock1.Setup(m => m.GetAll()).Returns(products);
          
            //Here we are going to mock repository Add method
            mock1.Setup(m => m.Add(It.IsAny())).Returns((Product target) =>
            {
                var original = products.FirstOrDefault(
                    q => q.ProductId == target.ProductId);

                if (original != null)
                {
                    return false;
                }

                products.Add(target);

                return true;
            });
          
            //Now we have our repository ready for property injection

            //Here we are going to mock our IUnitOfWork
            Mock mock = new Mock();
            

            //Here we are going to inject our repository to the property 
            //mock.SetupProperty(m => m.ProductRepository).SetReturnsDefault(mock1.Object);
           mock.Setup(m => m.ProductRepository).Returns(mock1.Object);

            //Now our UnitOfWork is ready to be injected to the service
            //Here we inject UnitOfWork to constractor of our service
            ProductService productService = new ProductService(mock.Object);


            //Act
            productService.AddProduct(new Product
                                          {
                                              ProductId = 7,
                                              ProductName = "P7Name",
                                              Description = "P7"
                                          });
            var result = productService.GetAllProduct();
            var newProduct = result.FirstOrDefault(t => t.ProductId == 7);



            //Assert

            Assert.AreEqual(products.Count, result.Count);
            Assert.AreEqual("P7Name", newProduct.ProductName);
            Assert.AreEqual("P7", newProduct.Description);
        }

        [TestMethod]
        public void Can_Get_All_Product_Service()
        {
            //Arrange
            List products = new List()
                                         {
                                             new Product(){Description="P1",ProductId=1,ProductName="p1Name"},
                                             new Product(){Description="P2",ProductId=2,ProductName="p2Name"},
                                             new Product(){Description="P3",ProductId=3,ProductName="p3Name"},
                                             new Product(){Description="P4",ProductId=4,ProductName="p4Name"},
                                             new Product(){Description="P5",ProductId=5,ProductName="p5Name"},
                                             new Product(){Description="P6",ProductId=6,ProductName="p6Name"},

                                         };
            Mock> mock1 = new Mock>();

            //Here we are going to mock repository GetAll method 
            mock1.Setup(m => m.GetAll()).Returns(products);

          
            //Now we have our repository ready for property injection

            //Here we are going to mock our IUnitOfWork
            Mock mock = new Mock();

            //Here we are going to inject our repository to the property 
            mock.Setup(m => m.ProductRepository).Returns(mock1.Object);


            //Now our UnitOfWork is ready to be injected to the service
            //Here we inject UnitOfWork to constractor of our service
            ProductService productService = new ProductService(mock.Object);


            //Act
            var result = productService.GetAllProduct();
            //Assert
            Assert.AreEqual(products,result);
        }

        [TestMethod]
        public void Can_Update_Product_Service()
        {
            //Arrange
            List products = new List()
                                         {
                                             new Product(){Description="P1",ProductId=1,ProductName="p1Name"},
                                             new Product(){Description="P2",ProductId=2,ProductName="p2Name"},
                                             new Product(){Description="P3",ProductId=3,ProductName="p3Name"},
                                             new Product(){Description="P4",ProductId=4,ProductName="p4Name"},
                                             new Product(){Description="P5",ProductId=5,ProductName="p5Name"},
                                             new Product(){Description="P6",ProductId=6,ProductName="p6Name"},

                                         };
            Mock> mock1 = new Mock>();

           
            //Here we are going to mock repository Edit method
            mock1.Setup(m => m.Edit(It.IsAny())).Returns((Product target) =>
            {
                var original = products.FirstOrDefault(
                    q => q.ProductId == target.ProductId);

                if (original == null)
                {
                    return false;
                }

                original.ProductName = target.ProductName;

                original.Description = target.Description;

                return true;
            });

           
            //Now we have our repository ready for property injection

            //Here we are going to mock our IUnitOfWork
            Mock mock = new Mock();

            //Here we are going to inject our repository to the property 
            mock.Setup(m => m.ProductRepository).Returns(mock1.Object);
            //Now our UnitOfWork is ready to be injected to the service
            //Here we inject UnitOfWork to constractor of our service
            ProductService productService = new ProductService(mock.Object);

            //Act
            var updatedProduct = new Product() {ProductId = 1, ProductName = "p1NameModified", Description = "p1Modified"};
            productService.UpdateProduct(updatedProduct);
            var actualProduct = products.FirstOrDefault(t => t.ProductId == 1);
            //Assert
            
            Assert.AreEqual(actualProduct.ProductName ,updatedProduct.ProductName);
            Assert.AreEqual(actualProduct.Description, updatedProduct.Description);
        }

        [TestMethod]
        public void Can_Delete_Product_Service()
        {
            //Arrange
            List products = new List()
                                         {
                                             new Product(){Description="P1",ProductId=1,ProductName="p1Name"},
                                             new Product(){Description="P2",ProductId=2,ProductName="p2Name"},
                                             new Product(){Description="P3",ProductId=3,ProductName="p3Name"},
                                             new Product(){Description="P4",ProductId=4,ProductName="p4Name"},
                                             new Product(){Description="P5",ProductId=5,ProductName="p5Name"},
                                             new Product(){Description="P6",ProductId=6,ProductName="p6Name"},

                                         };
            Mock> mock1 = new Mock>();

            //Here we are going to mock repository FindById Method
            mock1.Setup(m => m.FindById(It.IsAny())).Returns((int i) => products.Single(x => x.ProductId == i));

            //Here we are going to mock repository Delete method
            mock1.Setup(m => m.Delete(It.IsAny())).Returns((Product target) =>
            {

                var original = products.FirstOrDefault(
                    q => q.ProductId == target.ProductId);

                if (original == null)
                {
                    return false;
                }

                products.Remove(target);

                return true;
            });
          
            //Now we have our repository ready for property injection

            //Here we are going to mock our IUnitOfWork
            Mock mock = new Mock();

            //Here we are going to inject our repository to the property 
            mock.Setup(m => m.ProductRepository).Returns(mock1.Object);


            //Now our UnitOfWork is ready to be injected to the service
            //Here we inject UnitOfWork to constractor of our service
            ProductService productService = new ProductService(mock.Object);


            //Act

            productService.DeleteProduct(6);
            var result = products.FirstOrDefault(t => t.ProductId == 6);
            //Assert
            Assert.IsNull(result);
            Assert.IsTrue(products.Count ==5);
        }

        [TestMethod]
        public void Can_Find_By_Id()
        {
            //Arrange
            List products = new List()
                                         {
                                             new Product(){Description="P1",ProductId=1,ProductName="p1Name"},
                                             new Product(){Description="P2",ProductId=2,ProductName="p2Name"},
                                             new Product(){Description="P3",ProductId=3,ProductName="p3Name"},
                                             new Product(){Description="P4",ProductId=4,ProductName="p4Name"},
                                             new Product(){Description="P5",ProductId=5,ProductName="p5Name"},
                                             new Product(){Description="P6",ProductId=6,ProductName="p6Name"},

                                         };
            Mock> mock1 = new Mock>();

            //Here we are going to mock repository FindById Method
            mock1.Setup(m => m.FindById(It.IsAny())).Returns((int i) => products.Single(x => x.ProductId == i));

            //Now we have our repository ready for property injection
            //Here we are going to mock our IUnitOfWork
            Mock mock = new Mock();

            //Here we are going to inject our repository to the property 
            mock.Setup(m => m.ProductRepository).Returns(mock1.Object);

            //Now our UnitOfWork is ready to be injected to the service
            //Here we inject UnitOfWork to constractor of our service
            ProductService productService = new ProductService(mock.Object);


            //Act
            var result = productService.GetProduct(6);
            var expected = products.Single(t => t.ProductId == 6);

            //Assert
            Assert.AreEqual(expected,result);
        }
    }


7. Implementing Controller unit test

In implementing test on your controller you can achieve UI Test simply, for this purpose I have created sample tests as shown below.
// ProductControllerTest.cs
[TestClass]
    public class ProductControllerTest
    {
       [TestMethod]
        public void Can_Paginate()
       {
           //Arrange
           Mock mock= new Mock();
           var products = new List();
           
           mock.Setup(m => m.GetAllProduct()).Returns((new Product[]
                                                          {
                                                              new Product(){ProductId=1,ProductName = "P1"} ,
                                                               new Product(){ProductId=2,ProductName = "P2"} ,
                                                                new Product(){ProductId=3,ProductName = "P3"} ,
                                                                 new Product(){ProductId=4,ProductName = "P4"} ,
                                                                  new Product(){ProductId=5,ProductName = "P5"} ,    
                                                          }).ToList());
     
           ProductController controller=new ProductController(mock.Object);
           controller.PageSize = 3;

           //Act
           ProductListViewModel result = (ProductListViewModel)controller.Index(2).Model;

           //Assert
           Product[] prodArray = result.Products.ToArray();
           Assert.IsTrue(prodArray.Length==2);
           Assert.AreEqual(prodArray[0].ProductName,"P4");
           Assert.AreEqual(prodArray[1].ProductName,"P5");
       }

        [TestMethod]
        public void Can_Generate_Page_Links()
        {
            //Arrange -define an Html helper -we need to do this
            //in order to apply the extension method
            HtmlHelper myHelper = null;

            //Arrange -create PagingInfo data
            PagingInfo pagingInfo = new PagingInfo()
                                        {
                                            CurrentPage = 2,
                                            TotalItems = 28,
                                            ItemsPerPage = 10
                                        };

            //Arrange -set up the delegate using a lamda expression
            Func pageUrlDelegate = i => "Page" + i;

            //Act
            MvcHtmlString result = myHelper.PageLinks(pagingInfo, pageUrlDelegate);

            //Assert
            Assert.AreEqual(result.ToString(),@"1"
                +@"2"+
                @"3");
        }

        [TestMethod]
        public void Can_Send_Pagination_View_Model()
        {
            Mock mock = new Mock();          

            mock.Setup(m => m.GetAllProduct()).Returns((new Product[]
                                                          {
                                                              new Product(){ProductId=1,ProductName = "P1"} ,
                                                               new Product(){ProductId=2,ProductName = "P2"} ,
                                                                new Product(){ProductId=3,ProductName = "P3"} ,
                                                                 new Product(){ProductId=4,ProductName = "P4"} ,
                                                                  new Product(){ProductId=5,ProductName = "P5"} ,
                                                          }).ToList());

            ProductController controller = new ProductController(mock.Object);
            controller.PageSize = 3;

            //Act
            ProductListViewModel result = (ProductListViewModel) controller.Index(2).Model;

            //Assert
            PagingInfo pagingInfo = result.PagingInfo;
            Assert.AreEqual(pagingInfo.CurrentPage,2);
            Assert.AreEqual(pagingInfo.ItemsPerPage,3);
            Assert.AreEqual(pagingInfo.TotalItems,5);
            Assert.AreEqual(pagingInfo.TotalPages,2);
        }
    }

Không có nhận xét nào: