I'm trying to build a Web API using best practices, based on a simple proof-of-concept using students.
My goal is to build the system using:
- Onion Architecture to seperate concerns.
- Service-Repository for interaction between BLL and DAL.
- Autofac for IoC, to control injected dependencies.
- NPoco for micro-ORM.
- Moq for mocking dependencies in unit tests.
One thing I'd like to add is a Unit Of Work using database transactions.
Please tell me what you think so I can expand my knowledge and get better.
Project consists of:
- Common library containing DTO's and exceptions that are reused between BLL and API layer.
- DAL library containing everything for data access: Entities, repositories and a database factory.
- BLL library containing all services that the API layer uses.
- API library containing everything for the HTTP layer: Controllers, IoC containers, etc.
DAL
Starting with the Student entity using NPoco filters/decorators:
[TableName("Students")]
public class Student
{
[Column("ID")]
public int ID { get; set; }
[Column("Name")]
public string Name { get; set; }
[Column("Age")]
public int Age { get; set; }
}
My interface for the Student Repository:
public interface IStudentRepository
{
List<Models.Entities.Student> GetAllStudents();
Models.Entities.Student GetStudentByID(int id);
void AddStudent(Models.Entities.Student student);
}
My DbFactory interface, to get the NPoco database connection object:
public interface IDbFactory
{
IDatabase GetConnection();
}
My DbFactory implementation:
public class DbFactory : IDbFactory
{
private readonly string _connectionString;
public DbFactory(string connectionString)
{
_connectionString = connectionString;
}
public IDatabase GetConnection()
{
return new Database(_connectionString);
}
}
My Student Repository, injected with the DbFactory:
public class StudentRepository : IStudentRepository
{
private readonly IDbFactory _dbFactory;
public StudentRepository(IDbFactory dbFactory)
{
_dbFactory = dbFactory;
}
public List<Models.Entities.Student> GetAllStudents()
{
using (var db = _dbFactory.GetConnection())
{
return db.Fetch<Models.Entities.Student>();
}
}
public Models.Entities.Student GetStudentByID(int id)
{
using (var db = _dbFactory.GetConnection())
{
return db.SingleById<Models.Entities.Student>(id);
}
}
public void AddStudent(Models.Entities.Student student)
{
using (var db = _dbFactory.GetConnection())
{
db.Insert<Models.Entities.Student>(student);
}
}
}
BLL
My StudentService class, that uses a repository to get data from some store:
public class StudentService
{
private IStudentRepository _studentRepository;
public StudentService(IStudentRepository studentRepository)
{
_studentRepository = studentRepository;
}
public List<StudentDTO> GetAllStudents()
{
var students = _studentRepository.GetAllStudents();
return students.ToDTO<StudentDTO>().ToList();
}
public StudentDTO GetStudentByID(int id)
{
var student = _studentRepository.GetStudentByID(id);
return student.ToDTO<StudentDTO>();
}
public void AddStudent(StudentDTO student)
{
var studentEntity = student.ToDTO<Student>();
_studentRepository.AddStudent(studentEntity);
}
}
API
Here's my StudentController:
[RoutePrefix("students")]
public class StudentController : ApiController
{
private readonly IStudentRepository _studentRepository;
public StudentController(IStudentRepository studentRepository)
{
_studentRepository = studentRepository;
}
[HttpGet]
[Route("")]
public List<StudentDTO> GetAllStudents()
{
var studentService = new StudentService(_studentRepository);
var students = studentService.GetAllStudents();
return students;
}
[HttpGet]
[Route("{id}")]
public StudentDTO GetStudentByID(int id)
{
var studentService = new StudentService(_studentRepository);
var student = studentService.GetStudentByID(id);
return student;
}
}
Here's my ContainerConfig for IoC with Autofac, to control dependencies:
public static class ContainerConfig
{
public static IContainer Configure()
{
var builder = new ContainerBuilder();
// Register your Web API controllers.
builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
builder.RegisterType<DbFactory>().As<IDbFactory>()
.WithParameter("connectionString", "DB");
builder.RegisterType<StudentRepository>().As<IStudentRepository>();
// Set the dependency resolver to be Autofac.
var container = builder.Build();
return container;
}
}
Tests
Here are my Unit Tests for the Student Repository:
[TestClass]
public class StudentRepositoryTest
{
private Mock<IDbFactory> _mockDbFactory;
private Mock<IDatabase> _mockDatabase;
// System Under Test (SUT)
private IStudentRepository _studentRepository;
[TestInitialize]
public void Setup()
{
_mockDatabase = new Mock<IDatabase>();
_mockDatabase.VerifyAll();
_mockDbFactory = new Mock<IDbFactory>();
_mockDbFactory.Setup(x => x.GetConnection())
.Returns(_mockDatabase.Object);
_studentRepository = new StudentRepository(_mockDbFactory.Object);
}
[TestMethod]
public void GetAllStudentsTest()
{
// Act
var students = _studentRepository.GetAllStudents();
// Assert
_mockDatabase.Verify(x => x.Fetch<DAL.Models.Entities.Student>(), Times.Once);
}
[TestMethod]
public void GetSingleStudentTest()
{
// Arrange
int studentID = 1;
// Act
var students = _studentRepository.GetStudentByID(studentID);
// Assert
_mockDatabase.Verify(x => x.SingleById<DAL.Models.Entities.Student>(studentID), Times.Once);
}
[TestMethod]
public void InsertStudentTest()
{
// Arrange
DAL.Models.Entities.Student newStudent = new DAL.Models.Entities.Student
{
ID = 2,
Name = "Conor McGregor",
Age = 30
};
// Act
_studentRepository.AddStudent(newStudent);
// Assert
_mockDatabase.Verify(x => x.Insert<DAL.Models.Entities.Student>(newStudent), Times.Once);
}
}
Here are my tests for the Student Service:
[TestClass]
public class StudentServiceTest
{
private Mock<IStudentRepository> _mockStudentRepository;
// System Under Test (SUT)
private StudentService _studentService;
[TestInitialize]
public void Setup()
{
List<DAL.Models.Entities.Student> inMemoryStudentList = new List<DAL.Models.Entities.Student>
{
new DAL.Models.Entities.Student
{
ID = 1,
Name = "John Doe",
Age = 28
}
};
_mockStudentRepository = new Mock<IStudentRepository>();
_mockStudentRepository.Setup(x => x.GetAllStudents())
.Returns(inMemoryStudentList);
_mockStudentRepository.Setup(x => x.GetStudentByID(It.IsAny<int>()))
.Returns(inMemoryStudentList[0]);
_mockStudentRepository.Setup(x => x.AddStudent(It.IsAny<DAL.Models.Entities.Student>()))
.Callback((DAL.Models.Entities.Student student) => inMemoryStudentList.Add(student));
_studentService = new StudentService(_mockStudentRepository.Object);
}
[TestMethod]
public void GetAllStudentsTest()
{
// Act
var students = _studentService.GetAllStudents();
// Assert
_mockStudentRepository.Verify(x => x.GetAllStudents(), Times.Once);
Assert.IsInstanceOfType(students, typeof(List<StudentDTO>), "Return type is not correct");
Assert.AreEqual(1, students.Count, "Student count is not correct");
}
[TestMethod]
public void GetSingleStudentTest()
{
// Arrange
int studentID = 1;
// Act
var student = _studentService.GetStudentByID(studentID);
// Assert
_mockStudentRepository.Verify(x => x.GetStudentByID(studentID), Times.Once);
Assert.IsInstanceOfType(student, typeof(StudentDTO), "Return type is not correct");
Assert.AreEqual("John Doe", student.Name, "Student name is not correct");
Assert.AreEqual(28, student.Age, "Student age is not correct");
}
[TestMethod]
public void InsertStudentTest()
{
// Arrange
StudentDTO newStudent = new StudentDTO
{
ID = 2,
Name = "Conor McGregor",
Age = 30
};
// Act
_studentService.AddStudent(newStudent);
// Assert
_mockStudentRepository.Verify(x => x.AddStudent(It.IsAny<DAL.Models.Entities.Student>()), Times.Once);
var students = _studentService.GetAllStudents();
Assert.AreEqual(2, students.Count, "Student count is not correct");
}
}
Here are my tests for the Student Controller:
[TestClass]
public class StudentControllerTest
{
private Mock<IStudentRepository> _mockStudentRepository;
// System Under Test (SUT)
private StudentController _studentController;
[TestInitialize]
public void Setup()
{
List<DAL.Models.Entities.Student> inMemoryStudentList = new List<DAL.Models.Entities.Student>
{
new DAL.Models.Entities.Student
{
ID = 1,
Name = "John Doe",
Age = 28
}
};
_mockStudentRepository = new Mock<IStudentRepository>();
_mockStudentRepository.Setup(x => x.GetAllStudents())
.Returns(inMemoryStudentList);
_mockStudentRepository.Setup(x => x.GetStudentByID(It.IsAny<int>()))
.Returns(inMemoryStudentList[0]);
_studentController = new StudentController(_mockStudentRepository.Object);
}
[TestMethod]
public void GetAllStudentsTest()
{
// Act
var students = _studentController.GetAllStudents();
// Assert
_mockStudentRepository.Verify(x => x.GetAllStudents(), Times.Once);
Assert.IsInstanceOfType(students, typeof(List<StudentDTO>), "Return type is not correct");
Assert.AreEqual(1, students.Count, "Student count is not correct");
}
[TestMethod]
public void GetSingleStudentTest()
{
// Arrange
int studentID = 1;
// Act
var student = _studentController.GetStudentByID(studentID);
// Assert
_mockStudentRepository.Verify(x => x.GetStudentByID(studentID), Times.Once);
Assert.IsInstanceOfType(student, typeof(StudentDTO), "Return type is not correct");
Assert.AreEqual("John Doe", student.Name, "Student name is not correct");
Assert.AreEqual(28, student.Age, "Student age is not correct");
}
}