7
\$\begingroup\$

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 is on GitHub.

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");
    }
}
\$\endgroup\$
4
\$\begingroup\$

Initial and General Thoughts

Good job!

  • Built fine
  • Tests all passed
  • I like the project structure and naming

Source control - I find it easier to handle if packages not there, especially on a slow connection

  • Use nuget package restore

I generally use this .gitignore file for this type of project https://github.com/github/gitignore/blob/master/VisualStudio.gitignore

and to clear out files easily that are currently in a repo I'd use this method: https://stackoverflow.com/questions/1139762/ignore-files-that-have-already-been-committed-to-a-git-repository

Can then run consolidate and update on packages (there is a newer version of NPoco.. from 2 to 3). Right click on solution and manage packages

I've put up my refactored code here: https://bitbucket.org/davemateer/perfectpattern/overview

Pipeline

Continuous Integration - sanity check of a project. I generally always do this. Your code built fine first try on my machine - however this is not always the case. I use Appveyor as it is free for public repos. The only change I had to make from their default setup was:

in build section, before build script, appveyor-retry nuget restore https://ci.appveyor.com/project/djhmateer/perfectpattern

it runs your tests (your project is running fine)

Code

organse and get rid of unused usings (alt e i o a)

removed unused references: http://deanhume.com/home/blogpost/visual-studio---why-should-i-remove-unused-references-/6102 careful of the web project - tend to leave that alone

Student namespace

I recommend not having the namespace and the entity name the same:

was: namespace PerfectPattern.DAL.Models.Entities.Student now: namespace PerfectPattern.DAL.Models.Entities

so, don't have to reference Models.Entities.Student everywhere

public interface IStudentRepository
{
    List<Models.Entities.Student> GetAllStudents();
    Models.Entities.Student GetStudentByID(int id);
    void AddStudent(Models.Entities.Student student);
}

to

public interface IStudentRepository
{
    List<Student> GetAllStudents();
    Student GetStudentByID(int id);
    void AddStudent(Student student);
}

BLL/DAL and DTO's

So now this is pure opinion on how to do things, and there is no 'Perfect Pattern' :-) Every project has different needs. I have been trying recently using a flatter structure, essentially combining the BLL/DAL together. This negates the pass through code I see a lot in legacy BLL layers.

Regarding DTO's - I now use a ViewModel naming convention, and generally have 1 VM for each 'page' (if you were using a website):

  • studentDTO would become StudentHomePageViewModel

What is not there

Setup deployment to 'prod'. In your case I'd put up an azurewebsite.net site and get appveyor deploying there automatically. This would prove you can deploy to a prod environment. It also gets you thinking about database changes and how to handle it.

If you look at the output of this build project https://ci.appveyor.com/project/djhmateer/booktech

You'll see I do a build, deploy an MSSQL database locally, run unit and integration tests, then deploy the database changes (as long as no data loss occurs) and code to Azure.

Database Project

I like the DB to be under source control. eg https://bitbucket.org/davemateer/booktech/src

DB Profiling

I like MiniProfiler - helps me a lot in developing projects with significant data. enter image description here

I generally use Dapper for micro ORMS.. interesting to see NPoco!

Closing thoughts / Architecture

I work in enterprise software, and generally make my software only as complex as it needs to be. Generally because it will last for many years, with many developers looking at it. I make this point because your codebase is easy to understand currently, so depending on the actual application, you may not want to add any other parts. In fact, using 'Pure DI' instead of an IoC would be a thought (contentious)! http://blog.ploeh.dk/2012/11/06/WhentouseaDIContainer/

Hope this helps.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.