I am looking at updating some of my companies existing code to allow unit tests to be implemented. To be able to do this, all repositories are being interfaced to allow DI. However, the existing code has a habit of creating 1 repository class per table in the database, meaning that one section of business logic may need to query 5 different repositories classes.
Passing in 5+ interfaces using DI seems messy and I am looking for help on how to redesign this.
This is all for an MVC 5 C# site, and an example of it is below (I have reduced the code size so that it is easier to read).
We have our controller for Account, which in this scenario manages the user sessions:
public class AccountController : Controller
{
private AccountLogic _accountLogic { get; set; }
public AccountController(
ISessionRepository sessionRepository,
ILoginRepository loginRepository)
{
_accountLogic = new AccountLogic(sessionRepository, loginRepository);
}
public IActionResult Index()
{
return View();
}
public IActionResult Login()
{
return View();
}
public IActionResult Login(LoginModel loginModel)
{
if (ModelState.IsValid)
{
if (_accountLogic.DoesUserExist(loginModel.Username))
{
var existingHash= _accountLogic.GetPassword(loginModel.Username);
if (Hash.VerifyPassword(loginModel.Password, existingHash))
{
// Set Auth Cookies
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError(
"Username",
"Sorry, we did not recognize " +
"either your username or password");
}
}
}
return View(loginModel);
}
}
The Team tries to reduce the size of the controller by having a business class perform most of the actions. What this actually means is that the business class is very large and has many responsibilities as it has to be passed an interface for all of the repositories it has to query. In the example shown, we pass in both the Session and Login repositories, each of this is solely responsible for their corresponding tables in the database.
public class AccountLogic
{
private ISessionRepository _sessionRepository { get; set; }
private ILoginRepository _loginRepository { get; set; }
public AccountLogic(
ISessionRepository sessionRepository,
ILoginRepository loginRepository)
{
_sessionRepository = sessionRepository;
_loginRepository = loginRepository;
}
public Guid CreateSession(int loginID, string userAgent, string locale)
{
return _sessionRepository.CreateSession(loginID, userAgent, locale);
}
public Session GetSession(Guid sessionKey)
{
return _sessionRepository.GetSession(sessionKey);
}
public void EndSession(Guid sessionKey)
{
_sessionRepository.EndSession(sessionKey);
}
public bool DoesUserExist(string username)
{
return _loginRepository.DoesUserExist(username);
}
public string GetPassword(string username)
{
return _loginRepository.GetPassword(username);
}
}
We then have the AccountLogic class, which handles all business logic. In the example given, there is no complicated logic to be performed, so it becomes purely a chain call to the interface methods.
My question is, how do we make this easier to handle will also being testable on a larger scale? If I had a controller which needs to query 10 different repositories, surely it is not manageable to pass 15 interfaces to the controller, and then to a relevant business class? Do we need to change how repositories are handled so that there is not one class per table?
Best Answer
While not entirely in the style of C#, free monad API can help you here. Here's a C# example.
In a free monad API, your interface is modeled as objects:
This allows you to represent your entire data interface uniformly in a single file while allowing complete separation of the interface's implementation. You write programs by composing your interface objects in a free monad:
Interpreters look like big switch statements (pseudocode):
One of the benefits of using this pattern is that you can separate your DB tables separately in the system, but you can interpret them all as one DSL. That means in test you can provide a single mock interpreter to handle all your expected DB calls (and throw an exception if an unexpected call is made) (pseudocode):
Instead of providing numerous interface mocks.