Repository Design Pattern : Implementing Generics, MOQ, MSTEST In ASP.NET MVC

Repository Design Pattern is a way to encapsulate repetitive data access code.

Repository Pattern is effective data access design pattern when we want to:
  • Increase testability of the application. Make test repeatable without touching the data source;
  • Implement Separation of Concerns between the business logic and data source or business logic and test units;
  • Make the application work with different data source such SQL Server, mySQL, Oracle, web api endpoints, xml file and other RDBMS out in the market.
  • Make our code reusable. Effective on CRUD.
Repository is simply Persistent Ignorant, it does not care what data store it will be using. It works on in-memory data instead of the actual data coming from files.

Today, we are going to see how to implement this in ASP.NET MVC.



THE ARCHITECTURE

We are going to use Domain Centric Design architecture in this application so we need the following layers:
  • Core = Contains the business logic that includes the Generic Repository interface and the  POCO models.
  • Data = Layer that handles SQL Data Access
  • MVC = The UI
  • Test = Unit Tests
Architecture[8]

THE CORE LAYER

The POCO Models:

Country.cs

using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace ContactBook.Core.Models
{
public class Country
{
[Key]
public int Id { get; set; }
[Required(ErrorMessage = "*")]
[MaxLength(40)]
[DisplayName("Country")]
public string CountryName { get; set; }
public virtual ICollection<Client> Clients { get; set; }
}
}
view raw RepoCountry.cs hosted with ❤ by GitHub

Client.cs
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContactBook.Core.Models
{
public class Client
{
[Key]
public int id { set; get; }
[Required(ErrorMessage = "*")]
[MaxLength(30)]
[DisplayName("Last Name")]
public string LastName { set; get; }
[Required(ErrorMessage = "*")]
[MaxLength(30)]
[DisplayName("First Name")]
public string FirstName { set; get; }
[Required(ErrorMessage = "*")]
[MaxLength(40)]
[DisplayName("Address")]
public string Address { set; get; }
[MaxLength(30)]
[DisplayName("Contact #")]
public string ContactNo { set; get; }
[Required(ErrorMessage = "*")]
[MaxLength(100)]
[DataType(DataType.EmailAddress)]
[DisplayName("Email")]
public string Email { set; get; }
[DisplayName("Country")]
public int CountryId { get; set; }
[DisplayName("Remarks")]
[StringLength(300)]
[DataType(DataType.MultilineText)]
public string Remarks { get; set; }
[ForeignKey("CountryId")]
public virtual Country Country { get; set; }
}
}
view raw RepoClient.cs hosted with ❤ by GitHub


The Generic Repository Interface

In order to implement Dependency Injection and mocks, we need to use interface.

The advantage of using generics in repository is that is it reusable. We can reuse the repository by passing the class as parameter.

THE DATA LAYER

In the Data Layer we will use EF First Code approach. This is where we will implement the generic repository. We can use this in both Country and Client class.

EFClientDb.cs
using System;
using System.Linq;
using System.Data.Entity;
using ContactBook.Core.Models;
using ContactBook.Core.Repository;
using System.Data.Entity.Validation;
namespace ContactBook.Data.Data
{
public class EFClientDb : DbContext, IGenericRepo
{
public DbSet<Client> Clients { get; set; }
public DbSet<Country> Countries { get; set; }
public IQueryable<T> Query<T>() where T : class
{
return Set<T>();
}
public IQueryable<T> QueryIncluding<T>(params System.Linq.Expressions.Expression<Func<T, object>>[] includeProperties) where T : class
{
IQueryable<T> query = Set<T>();
foreach (var includeProperty in includeProperties)
{
query = query.Include(includeProperty);
}
return query;
}
public T GetById<T>(int id) where T : class
{
return Set<T>().Find(id);
}
public void Add<T>(T entity) where T : class
{
Set<T>().Add(entity);
}
public void Update<T>(T entity) where T : class
{
Entry(entity).State = System.Data.Entity.EntityState.Modified;
}
public void Delete<T>(T entity) where T : class
{
Set<T>().Remove(entity);
}
public void Delete<T>(int id) where T : class
{
var entity = Set<T>().Find(id);
Set<T>().Remove(entity);
}
public bool Save()
{
try
{
return this.SaveChanges() > 0;
}
catch (DbEntityValidationException ex)
{
Console.WriteLine("============E R R O R ======================");
foreach (var entityValidationErrors in ex.EntityValidationErrors)
{
foreach (var validationError in entityValidationErrors.ValidationErrors)
{
Console.WriteLine("Property: {0} Error: {1}",
validationError.PropertyName, validationError.ErrorMessage);
}
}
//System.InvalidOperationException
Console.WriteLine("============E R R O R ======================");
return false;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}


THE MVC

For the purpose of demonstration, we will be having some business logic in the controller level. But in production, it is recommended to push this business logic to the core layer.

This is where the Generic Repository is at its best. We can just change the parameter with the class we are working without adding another layer of abstraction in order to be specific on the entity we are working on.
ClientDb.Query<Country>().ToList()
view raw RepoQuery.cs hosted with ❤ by GitHub

CountryController.cs
using System.Linq;
using System.Web.Mvc;
using ContactBook.Core.Models;
using ContactBook.Data.Data;
using ContactBook.Core.Repository;
namespace ContactBook.MVC.Controllers
{
public class CountryController : Controller
{
private IGenericRepo ClientDb;
public CountryController()
{
ClientDb = new EFClientDb();
}
public CountryController(IGenericRepo clientDb)
{
ClientDb = clientDb;
}
// GET: /Country/
public ActionResult Index()
{
return View(ClientDb.Query<Country>().ToList());
}
// GET: /Country/Details/5
public ActionResult Details(int id)
{
Country country = ClientDb.GetById<Country>(id);
if (country == null)
{
return HttpNotFound();
}
return View(country);
}
// GET: /Country/Create
public ActionResult Create()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include="Id,CountryName")] Country country)
{
if (ModelState.IsValid)
{
ClientDb.Add<Country>(country);
ClientDb.Save();
return RedirectToAction("Index");
}
else
{
ModelState.AddModelError("", "Error Saving Record. Please try again...");
}
return View(country);
}
// GET: /Country/Edit/5
public ActionResult Edit(int id)
{
Country country = ClientDb.GetById<Country>(id);
if (country == null)
{
return HttpNotFound();
}
return View(country);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include="Id,CountryName")] Country country)
{
if (ModelState.IsValid)
{
ClientDb.Update<Country>(country);
ClientDb.Save();
return RedirectToAction("Index");
}
else
{
ModelState.AddModelError("", "Error Saving Record. Please try again...");
}
return View(country);
}
// GET: /Country/Delete/5
public ActionResult Delete(int id)
{
Country country = ClientDb.GetById<Country>(id);
if (country == null)
{
return HttpNotFound();
}
return View(country);
}
// POST: /Country/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(Country country)
{
if (ModelState.IsValid)
{
ClientDb.Delete<Country>(country.Id);
ClientDb.Save();
return RedirectToAction("Index");
}
else
{
ModelState.AddModelError("", "Unable to delete record.");
}
return View(country);
}
}
}

We can also apply this approach to ClientController. I will leave this to the readers. Try it yourself.

With regards with the views for this controller. It work like a normal mvc views.


THE TEST

This is another endpoint that we could expose the Repository. We can use fake data to test the controllers but we will be using Moq framework to test the CountryController (it much easier).

MoqCountryController.cs

using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;
using Moq;
using ContactBook.Core.Models;
using ContactBook.Core.Repository;
using ContactBook.MVC.Controllers;
using System.Web.Mvc;
namespace ContactBook.Test
{
[TestClass]
public class MoqCountryController
{
private Mock<IGenericRepo> GenericToMock;
private IList<Country> Countries;
private CountryController countryConroller;
[TestInitialize]
public void TestInitialize()
{
GenericToMock = new Mock<IGenericRepo>();
Countries = new List<Country>();
countryConroller = new CountryController(GenericToMock.Object);
Countries.Add(new Country { Id = 1, CountryName = "Japan" });
Countries.Add(new Country { Id = 2, CountryName = "Singapore" });
}
[TestCleanup]
public void TestCleanup()
{
GenericToMock = null;
Countries = null;
countryConroller = null;
}
#region Test for Index
[TestMethod]
public void CountryController_Index()
{
//Arrange
GenericToMock
.Setup(r => r.Query<Country>())
.Returns(Countries.AsQueryable());
//Act
ViewResult result = countryConroller.Index() as ViewResult;
IEnumerable<Country> model = result.Model as IEnumerable<Country>;
// Assert
Assert.AreEqual(2, model.Count());
GenericToMock.Verify(x => x.Query<Country>(),
Times.Exactly(1));
}
#endregion End for Index
#region Test for Create
[TestMethod]
public void CountryController_Create()
{
//Arrange
//Act
ViewResult result = countryConroller.Create() as ViewResult;
// Assert
Assert.IsInstanceOfType(result, typeof(ViewResult));
}
[TestMethod]
public void CountryController_Create_Record_Redirect_To_Index_On_Success()
{
//Arrange
var _country = new Country() { Id=4, CountryName = "Netherland" };
// Act
var result = countryConroller.Create(_country) as RedirectToRouteResult;
// Assert
Assert.AreEqual("Index", result.RouteValues["Action"]);
GenericToMock.Verify(x => x.Add<Country>(It.IsAny<Country>()));
}
[TestMethod]
public void CountryController_Create_Save_Record_Save_Method_Was_Called_On_Success()
{
//Arrange
var _country = new Country() { Id = 4, CountryName = "Netherland" };
GenericToMock
.Setup(r => r.Save())
.Returns(true);
//Act
var result = countryConroller.Create(_country) as RedirectToRouteResult;
// Assert
Assert.AreEqual("Index", result.RouteValues["Action"]);
GenericToMock.Verify(x => x.Save());
}
[TestMethod]
public void CountryController_Create_Record_Return_View_On_Invalid()
{
//Arrange
var _country = new Country() { Id = 0, CountryName=""};
countryConroller.ViewData.ModelState.Clear();
countryConroller.ModelState.AddModelError("Error","Model is invalid.");
//Act
var result = countryConroller.Create(_country) as ViewResult;
//Assert
Assert.AreEqual("", result.ViewName);
}
[TestMethod]
public void CountryController_Create_Record_Return_Same_Model_On_Invalid()
{
//Arrange
var _country = new Country() { Id = 0, CountryName = "" };
countryConroller.ViewData.ModelState.Clear();
countryConroller.ModelState.AddModelError("Error", "Model is invalid.");
//Act
var result = countryConroller.Create(_country) as ViewResult;
var model = result.Model as Country;
//Assert
Assert.AreEqual(result.Model, _country);
Assert.AreEqual(0, model.Id);
//GenericToMock.
}
#endregion End for Create Test
#region Test for Details
[TestMethod]
public void CountryController_Details()
{
//Arrange
GenericToMock
.Setup(r => r.GetById<Country>(1))
.Returns(new Country{Id=1,CountryName="Japan"});
//Act
ViewResult result = countryConroller.Details(1) as ViewResult;
Country model = result.Model as Country;
// Assert
Assert.AreEqual(1, model.Id);
Assert.AreEqual("Japan", model.CountryName);
GenericToMock.Verify(x => x.GetById<Country>(It.IsAny<int>()));
}
[TestMethod]
public void CountryController_Details_Returns_404_If_No_Country_Code()
{
//Arrange
//Act
var result = countryConroller.Details(5);
//Assert
Assert.IsInstanceOfType(result, typeof(HttpNotFoundResult));
}
#endregion End for Test for Details
#region Test for Edit
[TestMethod]
public void CountryController_Edit_Returns_CountryModel_On_Valid()
{
//Arrange
GenericToMock
.Setup(r => r.GetById<Country>(1))
.Returns(new Country { Id = 1, CountryName = "Japan" });
//Act
ViewResult result = countryConroller.Edit(1) as ViewResult;
Country model = result.Model as Country;
// Assert
Assert.AreEqual(1, model.Id);
Assert.AreEqual("Japan", model.CountryName);
GenericToMock.Verify(x => x.GetById<Country>(It.IsAny<int>()));
}
[TestMethod]
public void CountryController_Edit_Returns_404_If_No_Country_Code_Found()
{
//Arrange
//Act
var result = countryConroller.Edit(5);
//Assert
Assert.IsInstanceOfType(result, typeof(HttpNotFoundResult));
}
[TestMethod]
public void CountryController_Edit_Save_Record_Redirect_To_Index_On_Success()
{
//Assert
var _country = new Country() { Id = 4, CountryName = "Netherland" };
// Act
var result = countryConroller.Edit(_country) as RedirectToRouteResult;
// Assert
Assert.AreEqual("Index", result.RouteValues["Action"]);
}
[TestMethod]
public void CountryController_Edit_Save_Record_Save_Method_Was_Called_On_Success()
{
//Arrange
var _country = new Country() { Id = 4, CountryName = "Netherland" };
GenericToMock
.Setup(r => r.Save())
.Returns(true);
//Act
var result = countryConroller.Edit(_country) as RedirectToRouteResult;
//Assert
GenericToMock.Verify(x => x.Save());
}
[TestMethod]
public void CountryController_Edit_Save_Record_Return_View_On_Invalid_Model()
{
//Arrange
countryConroller = new CountryController(GenericToMock.Object);
var _country = new Country() { Id = 0, CountryName = "" };
countryConroller.ViewData.ModelState.Clear();
countryConroller.ModelState.AddModelError("Error", "Model is invalid.");
//Act
var result = countryConroller.Edit(_country) as ViewResult;
//Assert
Assert.AreEqual("", result.ViewName);
}
[TestMethod]
public void CountryController_Edit_Save_Record_Return_Same_Model_On_Invalid()
{
//Arrange
var _country = new Country() { Id = 0, CountryName = "" };
countryConroller.ViewData.ModelState.Clear();
countryConroller.ModelState.AddModelError("Error", "Model is invalid.");
//Act
var result = countryConroller.Edit(_country) as ViewResult;
var model = result.Model as Country;
//Assert
Assert.AreEqual(result.Model, _country);
Assert.AreEqual(0, model.Id);
}
#endregion End for Test Edit
#region Test for Delete
[TestMethod]
public void CountryController_Delete_Returns_Same_Model_When_Found()
{
//Arrange
GenericToMock
.Setup(r => r.GetById<Country>(1))
.Returns(new Country { Id = 1, CountryName = "Japan" });
countryConroller = new CountryController(GenericToMock.Object);
//Act
ViewResult result = countryConroller.Delete(1) as ViewResult;
Country model = result.Model as Country;
// Assert
Assert.AreEqual(1, model.Id);
Assert.AreEqual("Japan", model.CountryName);
GenericToMock.Verify(x => x.GetById<Country>(It.IsAny<int>()));
}
[TestMethod]
public void CountryController_Delete_Return_httpNoFound_When_Country_Code_Not_Found()
{
//Arrange
//Act
var result = countryConroller.Delete(1);
//Assert
Assert.IsInstanceOfType(result, typeof(HttpNotFoundResult));
}
#endregion End for Delete Method
#region Test for DeleteConfirm
[TestMethod]
public void CountryController_DeleteConfirmed_Redirect_To_Index_On_Success()
{
//Assert
var _country = new Country() { Id = 4, CountryName = "Netherland" };
// Act
var result = countryConroller.DeleteConfirmed(_country) as RedirectToRouteResult;
// Assert
Assert.AreEqual("Index", result.RouteValues["Action"]);
}
[TestMethod]
public void CountryController_DeleteConfirmed_Post_Action_Returns_RedirectToAction()
{
//Arrange
GenericToMock.Setup(x =>
x.Delete<Country>(1));
var _country = new Country() { Id = 1, CountryName = "Japan" };
//Act
var result = countryConroller.DeleteConfirmed(_country);
//Assert
Assert.IsInstanceOfType(result, typeof(RedirectToRouteResult));
}
[TestMethod]
public void CountryController_DeleteConfirmed_Delete_Method_Was_Called()
{
//Arrange
var _country = new Country() { Id = 4, CountryName = "Netherland" };
GenericToMock
.Setup(r => r.Delete<Country>(4));
//.Returns(true);
//Act
var result = countryConroller.DeleteConfirmed(_country) as RedirectToRouteResult;
//Assert
GenericToMock.Verify(x => x.Delete<Country>(It.IsAny<int>()));
}
[TestMethod]
public void CountryController_DeleteConfirmed_Save_Method_Was_Called_On_Success()
{
//Arrange
var _country = new Country() { Id = 4, CountryName = "Netherland" };
GenericToMock
.Setup(r => r.Save())
.Returns(true);
//Act
var result = countryConroller.Edit(_country) as RedirectToRouteResult;
//Assert
GenericToMock.Verify(x => x.Save());
}
[TestMethod]
public void CountryController_DeleteConfirmed_Return_Same_Model_On_Not_Found_Error()
{
//Arrange
var _country = new Country() { Id = 0, CountryName = "" };
countryConroller.ViewData.ModelState.Clear();
countryConroller.ModelState.AddModelError("Error", "Model is invalid.");
//Act
var result = countryConroller.DeleteConfirmed(_country) as ViewResult;
var model = result.Model as Country;
//Assert
Assert.AreEqual(result.Model, _country);
Assert.AreEqual(0, model.Id);
Assert.IsTrue(countryConroller.ViewData.ModelState.Count == 2);
}
#endregion
}
}

That's it! Thank you for time. I hope this article is helpful. Share it with your friends.