-
Go to Northwind.Tests/Northwind.Data and add a new class called StaffMemberRepositoryTests; this will contain a single test to prove that our currently nonexistent, custom Repository method is working as expected:
using NUnit.Framework;
using SharpArch.Testing.NUnit.NHibernate;
using Northwind.Core.DataInterfaces;
using Northwind.Core;
using System.Collections.Generic;
using NUnit.Framework.SyntaxHelpers;
using Northwind.Data;
namespace Tests.Northwind.Data
{
[TestFixture]
[Category("DB Tests")]
public class StaffMemberRepositoryTests : RepositoryTestsBase
{
protected override void LoadTestData() {
AddStaffMember("thistest_Filter", "Mike", "Park");
AddStaffMember("ABC123", "_test_FiLtEr_", "Vance");
AddStaffMember("GHI789", "Lynette", "Knackstedt");
AddStaffMember("DEF456", "Gerry",
"Lundquistest_filtER");
}
[Test]
public void CanLoadStaffMembersMatchingFilter() {
List<StaffMember> matchingStaffMembers =
staffMemberRepository
.FindAllMatching("TEST_FiLtEr");
Assert.That(matchingStaffMembers.Count,
Is.EqualTo(3));
}
private void AddStaffMember(string employeeNumber,
string firstName, string lastName) {
StaffMember staffMember =
new StaffMember(employeeNumber) {
FirstName = firstName,
LastName = lastName
};
staffMemberRepository.SaveOrUpdate(staffMember);
FlushSessionAndEvict(staffMember);
}
private IStaffMemberRepository staffMemberRepository =
new StaffMemberRepository();
}
}
The benefit of inheriting from RepositoryTestsBase is twofold: during setup, an in-memory SQLite database is created and the NHibernate session factory is initialized. Within the LoadTestData, you can then insert data into this in-memory database which will be used by the unit tests. Note that after each staff member is inserted into the database, it is flushed and evicted. The flush commits the new staff member to the database and the eviction removes the staff memory from NHibernate’s radar. In this way, when you retrieve a staff member from the database, NHibernate makes a trip to the database without pulling the object from cache.
Also notice, at the top of the test class, an attribute has been included to indicate that this test is within the "DB Tests" category. Categorizing this test enables you to disable the running of these unit tests within NUnit. Why would you want to do that? (Well, we disabled them just a bit ago in order to focus a bit longer on the domain before moving on to the data layer.) Typically, you’d disable this category of tests because unit tests which connect to a database are slow, taking a few seconds to load and run. This doesn’t sound too bad until you have dozens or hundreds of DB tests to run. If all the tests take more than a few seconds to run, then developers stop running them. And once developers stop running them, tests start breaking and the quality of the code degrades. So by turning the DB tests off while running the domain logic tests allows a developer to run all of the fast tests very often and run the time consuming tests when appropriate. The continuous integration server will also be running all of the longer running tests every time a change is checked in, so they’ll get checked sooner rather than later anyway…assuming you’re a good coder who checks in changes frequently with no broken unit tests! Testing against SQLite greatly improves the performance, but it’s still nice to be able to turn them off in some situations.
Finally note, in the unit test, we’re asserting that we expect three matching staff members to be returned from the Repository method.
-
Since we’ve already created
IStaffMemberRepository, but haven’t created the StaffMemberRepository concrete implementation, this unit test will not compile. At this point, add a public StaffMemberRepository class to Northwind.Data and have it inherit from Repository<StaffMember> and implement IStaffMemberRepository. To get the solution to compile, you’ll need to also implement the FindAllMatching method from IStaffMemberRepository, but just throw a NotImplementedException for now.
using Northwind.Core;
using SharpArch.Data.NHibernate;
using Northwind.Core.DataInterfaces;
using System.Collections.Generic;
using System;
namespace Northwind.Data
{
public class StaffMemberRepository
: Repository<StaffMember>, IStaffMemberRepository
{
public List<StaffMember> FindAllMatching(string filter) {
throw new NotImplementedException();
}
}
}
-
Now back to NUnit and run the unit tests to see our
NotImplementedException faithfully being raised within the CanLoadStaffMembersMatchingFilter test.
-
To get the test to pass, we now need to fill in the details of our custom Repository method. As was done in a previous step, a new class, called
StaffMemberRepository was added to Northwind.Data which implements IStaffMemberRepository. Use the following to build out the remaining details of this class:
using Northwind.Core.DataInterfaces;
using Northwind.Core;
using System.Collections.Generic;
using NHibernate;
using SharpArch.Data.NHibernate;
using NHibernate.Criterion;
namespace Northwind.Data
{
public class StaffMemberRepository :
Repository<StaffMember>, IStaffMemberRepository
{
public List<StaffMember> FindAllMatching(string filter) {
ICriteria criteria =
Session.CreateCriteria(typeof(StaffMember))
.Add(
Expression.Or(Expression.InsensitiveLike("EmployeeNumber", filter, MatchMode.Anywhere),
Expression.Or(Expression.InsensitiveLike("FirstName", filter, MatchMode.Anywhere),
Expression.InsensitiveLike("LastName", filter, MatchMode.Anywhere)))
)
.AddOrder(
new NHibernate.Criterion.Order("LastName", true)
);
return criteria.List<StaffMember>() as List<StaffMember>;
}
}
}
The above code implements and inherits from two objects: IStaffMemberRepository which we’ve already seen, and Repository which comes from SharpArch.Data.NHibernate. The Repository base class implements IRepository and provides most of the basic Repository CRUD functionality that we’ll ever need. The benefit to inheriting from this base implementation in our custom Repository is that we only need to provide details for our custom method, FindAllMatching(string filter). Writing a custom Repository is usually the exception than the norm; more often than not, Repository will provide all the data access functionality we need for a given Entity. But you still have a few options to choose from when the need arises; in fact, you have three options when it comes to a using a data repository:
- You can use the concrete “out of the box” repository
SharpArch.Data.NHibernate.Repository, implementing SharpArch.Core.PersistenceSupport.IRepository, which includes Get, GetAll, GetByProperties, GetUniqueByProperties, SaveOrUpdate, and Delete - this will likely be the most common scenario. This also serves as a good base class for your own custom repositories.
- You can use the more NHibernate specific interface
SharpArch.Core.PersistenceSupport.NHibernate.INHibernateRepository which includes NHibernate specific additions to IRepository, including Get with a lock mode, Load with and without a lock mode, GetByExample, GetUniqueByExample, Save (for assigned IDs), Update (also for assigned IDs), and Evict. The concrete implementation to be used with this is SharpArch.Data.NHibernate.NHibernateRepository. But since controllers only depend on the repository interfaces, and not the underlying repository, the controllers’ layer sees them as different objects. Accordingly, for the benefit of keeping your layers loosely coupled to NHibernate, you should try to avoid using this more NHibernate specific interface if possible. But if you have an object with an assigned ID, it’s unavoidable as you’ll need the separate Save and Update methods.
- Alternatively, you can extend
IRepository and the Repository class with your own custom methods as we’re about to do.
-
While the above discusses the concrete implementations that are available for out of the box usage, it should be clarified that there are exactly four interfaces which are the basis for all repository objects (including your own custom repositories):
SharpArch.Core.IRepositoryWithTypedId<T, IdT>: This provides exposure to very basic generic repository methods; e.g., Get() and SaveOrUpdate(). It accepts the domain object domain, and the type of the domain object's ID property to communicate with the database.
SharpArch.Core.IRepository<T>: This is nothing more than IRepositoryWithTypedId with a default ID type of int. This is expected to be used almost exclusively and certainly more than any other repository.
SharpArch.Core.NHibernate.INHibernateRepositoryWithTypedId<T, IdT>: This extends the basic repository methods with other methods which are specific to NHibernate; e.g., GetByExample, Save, Update, etc. This would be a good interface to add GetByPage, and other non-trivial repository methods which have some insight about the internals of NHibernate. As a general rule, the use of this repository should be avoided unless absolutely necessary; e.g., if you're using a domain object with an assigned ID (ack!).
SharpArch.Core.NHibernate.INHibernateRepository<T>: Like IRepository<T>, this is nothing more than INHibernateRepositoryWithTypedId with a default ID type of int. And similarly to NHibernateRepositoryWithTypedId, the use of this repository should be avoided unless absolutely necessary.
-
In the
StaffMemberRepository code that you just wrote (or copy and pasted anyway), the querying mechanism uses Hibernate Query Language (HQL); for more information about using HQL, see the NHibernate reference documentation at http://www.hibernate.org/hib_docs/nhibernate/1.2/reference/en/html. My only (very big) complaint with HQL is that string literals are used instead of strongly typed references; a few good solutions exist to solve this problem:
- LINQ to NHibernate: This is still a work in progress, but it’s coming along very nicely. You can read more about this LINQ provider at http://www.hookedonlinq.com/LINQToNHibernate.ashx. For your convenience, NHibernate.Linq.dll has been included in the VS project template’s /lib folder and added to YourProject.Data as a reference. But, I would not recommend using it in production code until the NHibernate team has released a more mature version.
Typesafe ICriteria using Lambda Expressions: http://nhforge.org/blogs/nhibernate/archive/2009/01/07/typesafe-icriteria-using-lambda-expressions.aspx. This is useful as it gives you the expressiveness of HQL, which is quite powerful, while still being strongly typed.
- NHibernate Query Generator (which is now deprecated): http://www.ayende.com/Blog/archive/7186.aspx
-
With this table in place, and after a very long winded interlude, you should now be able to run all of the unit tests and see them pass. The
CanLoadStaffMembersMatchingFilter() test successfully connected to the SQLite database, created the database using schema information from our mapping classes, populated the StaffMembers table with test data, ran the provided filtering query, hydrated the objects with NHibernate’s help, and returned the results to the unit test as a strongly typed listing of StaffMember objects. No datasets ...no ADO.NET ...no connection object management…no stored procedures…no thousands of lines of auto-generated data access code!
-
There’s nothing to refactor at this point, but a five minute break is certainly warranted!