Introduction
A few months ago, I wrote an article in which I talked about creating an online exam using NHibernate
. In this article, I will use LINQ to Classes to create the online exam. This is a multi-series article and in this part, I will discuss the architecture and the design of the application.
Application Requirement
The client needs an application where students can log in and give the exam. The exam is uploaded using an XML file provided by the client. The user will take the exam, then view his scores.
I will not discuss the user login and authentication section and will focus on the exam modules.
Database Design
The database name is School
and it consists of five tables. Take a look at the database diagram shown below:
Users
: This table holds the users.
Exams
: This table holds the exams.
Questions
: This table holds the questions of the exams.
Choices
: This table holds the choices of the questions.
UserExams
: This table holds the user grade and score of the exams.
Class Diagram
LINQ to Classes allows you to create classes using the database tables. Simply, drag and drop the table from the server explorer on the design view and it will automatically create the class diagram. Take a look at the entity class diagram below:
Apart from the entity class diagram, I have also included the Repository
and Services
class diagram. This one is a big diagram so don't be scared!
Let me explain the architecture of the repositories. Each aggregate root has its own repository. Exam
has ExamRepository
, User
has UserRepository
etc. There are no repositories for Question
and Choices
. This is because they are handled by the ExamRepository
.
Each repository inherits from the particular repository interface and the BaseRepository
class. Every concrete repository inherits from the base repository interface called IBaseRepository
. This is because each concrete repository i.e. ExamRepository
, UserRepository
must expose some common methods like GetById
, Add
, PersistAll
, GetAll
. If I put these methods in the corresponding repository interfaces, then I have to implement those interfaces in the concrete repositories which means repetitive code.
The BaseRepository
comes to the rescue and implements the common methods exposed by all the repositories. The methods are also marked as virtual so they can be overridden in the sub repositories.
Let�s take a look at the IBaseRepository
interface:
public interface IBaseRepository
{
T GetById<t>(int id) where T : class;
void Add<t>(T item) where T : class;
void PersistAll();
void AddAndPersistAll<t>(T item) where T : class;
}
As you can see, the IBaseRepository
interface works with the generic type T
. I will explain the purpose of using the constraints later in this article. Now, let�s see the implementation of the generic GetById
method implemented in the BaseRepository
class:
public T GetById<t>(int id) where T : class
{
var table = school.GetTable<t>();
MetaModel mapping = table.Context.Mapping;
ReadOnlyCollection<metadatamember /> members = mapping.GetMetaType(typeof(T)).DataMembers;
string pk = (members.Single<metadatamember />(m => m.IsPrimaryKey)).Name;
return table.SingleOrDefault<t>
(delegate(T t)
{
int memberId = (int)t.GetType().GetProperty(pk).GetValue(t, null);
return memberId == id;
});
}
The GetById
method above looks a little complicated but I will try my best to explain it. First, let�s talk about the constraint as shown below:
public T GetById<t>(int id) where T : class
The constraint says that the type T
must be a class. This is because the method school.GetTable<t>()
only works on the reference types. The purpose of GetById
method is to return the object based on its Id. The problem is that each entity class has a different name for its primary key column. User
class has UserID
, Exam
class has ExamID
and so on. This makes things more complicated as we have to find the name of the column which serves as the primary key. For this, I am using the following code which finds all the columns of the table and then finds the column name which serves as the primary key.
MetaModel mapping = table.Context.Mapping;
ReadOnlyCollection<metadatamember /> members = mapping.GetMetaType(typeof(T)).DataMembers;
string pk = (members.Single<metadatamember />(m => m.IsPrimaryKey)).Name;
Once we get the primary key, we can extract the value out of that column and compare it with our passed parameter id
as shown below:
return table.SingleOrDefault<t>
(delegate(T t)
{
int memberId = (int)t.GetType().GetProperty(pk).GetValue(t, null);
return memberId == id;
});
There is a performance hit when using the above approach since we are using reflection to find the value. But this was a trade off I took over writing repetitive code.
Let�s also take a look at the other three BaseRepository
methods:
public void Add<t>(T item) where T : class
{
var table = school.GetTable<t>();
table.InsertOnSubmit(item);
}
public void PersistAll()
{
school.SubmitChanges();
}
public void AddAndPersistAll<t>(T item) where T : class
{
var table = school.GetTable<t>();
table.InsertOnSubmit(item);
PersistAll();
}
The Add<t>(T item)
method adds the item to the table collection but does not commit it to the database. The PersistAll
method commits the item to the database. Finally, the AddAndPersistAll
method adds and persists the item in the database.
Conclusion
In this article we discussed the architecture of the online exam application. In the next article, we will write some unit tests to test our domain layer and the repositories.
The download will be provided in the next part of this series.