A good deed

Looks like I did a good deed today :)

And despite Stephen's words, I did it out of gratitude, not out of pity :)

Repository implementation

Along with NHibernate mappings, Peniko.DataLayer project contains Repository implementations. Repository interfaces are in Peniko.Domain project, as mentioned before.

All communication with NHibernate is done through ISession instance. Session is basically a Unit of Work for NHibernate. We have a standard NHibernateHelper static class for encapsulating ISessionFactory and for creating / opening the session:

public static class NHibernateHelper

{

    private static ISessionFactory _sessionFactory;

 

    private static ISessionFactory SessionFactory

    {

        get

        {

            if (_sessionFactory == null)

            {

                var configuration = new Configuration();

                configuration.Configure();

                configuration.AddAssembly(typeof(Product).Assembly);

                _sessionFactory = configuration.BuildSessionFactory();

            }

            return _sessionFactory;

        }

    }

 

    public static ISession OpenSession()

    {

        return SessionFactory.OpenSession();

    }

}

SessionFactory is created from the NHibernate configuration the first time its accessed. Configuration comes from hibernate.cfg.xml file defined in UI and test projects:

<?xml version="1.0" encoding="utf-8" ?>

<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">

  <session-factory>

    <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>

    <property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>

    <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>

    <property name="connection.connection_string">...</property>

    <property name="show_sql">true</property>

    <mapping assembly="Peniko.DataLayer" />

  </session-factory>

</hibernate-configuration>

In Peniko.DataLayer, every Repository method uses its own session, so we can't use NHibernate's lazy loading feature. Better option would be session-per-conversation pattern mentioned here, but we'll leave it as is for now.

The most important class in Repository implementation is Repository<T> implementing our IRepository<T> interface:

public interface IRepository<T> where T : Entity<T>

{

    void Save(T entity);

    void Delete(T entity);

    T GetById(Guid id);

    IList<T> GetAll();

}

 

public abstract class Repository<T> : IRepository<T> where T : Entity<T>

{

    private static readonly ILog _log = LogManager.GetLogger(typeof (Repository<T>));

    protected static ILog Log { get { return _log; } }

 

    public void Save(T entity) { ... }

    public void Delete(T entity) { ... }

    public T GetById(Guid id) { ... }

    public IList<T> GetAll() { ...  }

}

It's defined as abstract class and provides inherited classes with basic strongly typed methods for saving, deleting and retrieving all entities or a single entity by id and also an log4net's ILog instance . Inherited class is defined like this:

public class ProductRepository : Repository<Product>, IProductRepository { ... }

This is a Save method from Repository<T>:

public void Save(T entity)

{

    if (!entity.IsValid) throw new ValidationException("Entity is invalid");

 

    using (var session = NHibernateHelper.OpenSession())

    using (var transaction = session.BeginTransaction())

    {

        Log.DebugFormat("Saving entity Id: {0}", entity.Id);

 

        try

        {

            session.SaveOrUpdate(entity);

            AfterSaveEntity(session, entity);

            transaction.Commit();

        }

        catch (StaleObjectStateException ex)

        {

            transaction.Rollback();

            Log.Error("Error saving entity", ex);

            throw new ConcurrencyException("The data has been modified by another user", ex);

        }

        catch (HibernateException ex)

        {

            transaction.Rollback();

            Log.Error("Error saving entity", ex);

            throw;

        }

    }

}

First thing it does is to check if entity is valid. Then it opens a session and begins a new transaction on it. As of NHibernate 2.0, all Saves, Updates and Deletes must be encapsulated in transaction. Save method calls session.SaveOrUpdate() which does both insert and update operations based on the value of entity.Id. In our case, if Id is Guid.Empty, it will do Insert, otherwise Update.

AfterSaveEntity call allows inherited class to do some action after the entity is saved successfully. I.e. to save some other, unrelated entity.

Note that we catch StaleObjectStateException. That exception is thrown when there is a concurrency problem. We encapsulate it in our ConcurrencyException and throw that instead. That way it's easy to catch it in UI code without dependency on NHibernate.

Delete method is more or less the same. The only difference is that it calls session.Delete() instead of session.SaveOrUpdate().

GetById looks like this:

public T GetById(Guid id)

{

    using (var session = NHibernateHelper.OpenSession())

    {

        Log.DebugFormat("Getting entities with Id: {0}", id);

 

        try

        {

            var entity = session.Get<T>(id);

            AfterGetEntity(session, entity);

            return entity;

        }

        catch (HibernateException ex)

        {

            Log.Error("Error getting entities by id", ex);

            throw;

        }

    }

}

This method is very simple. session.Get<T>(id) returns a single entity with the given id. GetAll method is similar, but it uses NHibernate Criteria API and returns session.CreateCriteria(typeof (T)).List<T>() as a result.

In another project I'm currently involved, Repository also has FindAll and FindOne methods that accept DetachedCriteria as a parameter. The idea came from 12. episode of Summer of NHibernate.

Other Repository classes inherited from Repository<T> have its own methods for retrieving entities. These methods are mostly using Criteria API. More on Criteria API and HQL in NHibernate learning material.

MappingsThe next thing to be done was NHibernate mapping. Mappings are defined in Peniko.DataLayer project.

Every entity class has its own mapping file. Mapping file is named after a class, so if class name is Category, the mapping file is Category.hbm.xml. hbm stands for Hibernate mapping. Having this naming scheme is not required, but NHibernate will try to look for these names automatically when mapping. If you want to name your mappings differently, then you'll need to tell that to NHibernate.

Mapping file must have Embedded Resource build action. That's the thing that gets easily forgotten, so double check all your mapping files.

Writing a mapping file can be tedious and error-prone, but there is a cure for that. NHibernate comes with two XML schema files (nhibernate-configuration.xsd and nhibernate-mapping.xsd) that you can copy to Program Files\Microsoft Visual Studio 9.0\Xml\Schemas. Once there, they'll provide you with Intelli-sense when writing NHibernate mappings and configuration in Visual Studio.

If you are ReSharper user, there is also a nice ReSharper NHibernate plugin. It checks type, assembly and property names and allows navigation to mapped classes with Ctrl+Click on class or property name. Also, any renames are reflected in mapping file.

Below is an example of a very simple mapping:

<?xml version="1.0" encoding="utf-8" ?>

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"

                  assembly="Peniko.Domain"

                  namespace="BLDotNet.Peniko.Domain">

  <class name="Category" table="Categories">

    <id name="Id" column="CategoryId">

      <generator class="guid" />

    </id>

 

    <property name="Name" column="Name" length="50" not-null="true" unique="true" />

  </class>

</hibernate-mapping>

You must fully qualify type names in mapping files ("Namespace.Class, Assembly"), or you can define assembly and namespace attributes on root tag.

If the table name differs from the class name, there has to be a table attribute on <class> tag. In our case, all table names are plural.

id must be defined for all mapped classes. There are different id generators in NHibernate, but you'll most likely choose between "guid" and "native". When "native" generator is selected, NHibernate will use database generated value - this is an identity field in SQL Server. NHibernate maintains ids itself, so you don't have to ever assign values to them (unless you are using "assigned" generator).

All simple properties are mapped with <property> tag. There are also <many-to-one>, <one-to-one> and <one-to-many> tags for relations with other mapped classes; <subclass> and <joined-subclass> for inheritance mapping; <map>, <set>, <list> and <bag> for collection mapping and even more tags.

This should be enough to get you started with NHibernate mapping. For more details see NHibernate learning material.

Unit Testing the Domain

This post is a follow-up to Are you valid?. Last time I tried to explain how the validation is done in our Domain project.

Peniko.Domain.Tests project contains Unit Tests for our Entity classes. The tests are written using NUnit library. Every entity class has its own test fixture. Below is a sample fixture for Category entity:

namespace BLDotNet.Peniko.Domain.Tests

{

    [TestFixture]

    public class CategoryFixture

    {

        private const int NameMaxLength = 50;

 

        private Category _category;

 

        [SetUp]

        public void SetUp()

        {

            _category = TestGlobals.GetValidCategory();

        }

 

        [Test]

        public void CanCreateCategory()

        {

            Assert.IsTrue(_category.IsValid);

            Assert.AreEqual("NewCategory", _category.Name);

        }

 

        [Test]

        public void CannotHaveNullName()

        {

            _category.Name = null;

            Assert.IsFalse(_category.IsValid);

        }

 

        [Test]

        public void CannotHaveEmptyName()

        {

            _category.Name = String.Empty;

            Assert.IsFalse(_category.IsValid);

        }

 

        [Test]

        public void CanHaveMaxLengthName()

        {

            _category.Name = new String('A', NameMaxLength);

            Assert.IsTrue(_category.IsValid);

        }

 

        [Test]

        public void CannotHaveTooBigName()

        {

            _category.Name = new String('A', NameMaxLength + 1);

            Assert.IsFalse(_category.IsValid);

        }

    }

}

And here is how the Category class looks like:

namespace BLDotNet.Peniko.Domain

{

    public class Category : Entity<Category>

    {

        [NotNullValidator]

        [StringLengthValidator(1, 50,

            MessageTemplateResourceName = "CategoryNameLength",

            MessageTemplateResourceType = typeof (Category))]

        public virtual string Name { get; set; }

    }

}

This case is very simple. Category has only one property - Name, apart from Id inherited from Entity class. The fixture only tests if the Category object can be created and if the validation is OK. Name property is tested for null and upper and lower length boundaries.

You'll notice that there is a SetUp() method that uses TestGlobals.GetValidCategory() method to create the Category to test. TestGlobals is a static class containing methods to create valid entities. We extracted entity creation from our test classes so we can reuse some entities as properties of other entities.

I.e. Project object has a Category property. TestGlobals.GetValidProject() calls TestGlobals.GetValidCategory() to assign to Category property. Some of you may ask now: Hey, what about mocking? Shouldn't you assign the mock Category to Project? Well, we are going one step at a time, no mocking frameworks learned yet :)

Most of our Domain tests looks like this. Some of the classes like OutputInvoiceDetail and InputInvoiceDetail have encapsulated calculations, so their tests are little more complicated, but not worth mentioning.

There are no Repository tests in Peniko.Domain.Tests project. Repositories are tested in separated DataLayer.Tests project. More on that later.

You are an idiot!

idiotI just had an interesting conversation with a friend of mine. He recently started to learn NHibernate, in fact we are working together on one application. 

Apparently, he got into an argument with his co-worker about NHibernate and O/RM in general. The co-worker of his is an experienced developer, the kind of people whose advice you would appreciate. And he had an advice. Don't use O/RM! It's slow, unnecessary complication which favors Java style of architecture. Oh, and it's extremely hard for debugging and maintenance! He used an O/RM in ONE project, and it's bad. Let's all avoid it.

Needles to say, but these arguments made my friend feel bad. Was it a bad decision to start learning NHibernate and to use it in an application?

The title of this post is a little harsh, but whenever I have an argument like this I always feel that the other person is trying to say to me that I'm an idiot. "Yes, you are!", you might said, "Why should it bother you. Don't take it to the heart." Well, what can I say ... It does bother me, so let me have my own arguments here.

  1. It's slow - He's right. O/RM is slower than plain ADO.NET code. Several percents or tens of percents. Hell, it's another abstraction layer, it has to be. .NET Framework applications are slower than C++ ones, and C++ applications are slower than assembler ones. Does this mean that we should ditch .NET in favor of assembler? Maybe, for a tiny performance critical application or parts of code, but for an enterprise application...? The benefits of an O/RM outweighs the performance impact in most situations.
  2. Unnecessary complication - I shouldn't even comment this, because I'm not sure what he meant or how was his application structured. In most cases I encountered, the developers (myself included) were more responsible for complicated applications than frameworks or tools. We often start using something just after looking at Hello World sample. Don't get me wrong, this can work, we learn as we go, but we should take a time to at least find and learn best practices.
    I still remember what came out of my first CSLA application. Boy, was it a mess.
    Or my first relational database application. It should have stored daily entries with details, plain master-detail case. I remember creating a table per day in code (Entry_10_05_99, Entry_10_06_99...), so each day details had it's own table. The case for The Daily WTF.
  3. Java style architecture - WTF?!? What's wrong with Java? I've never used it professionally, but still appreciate all ideas borrowed from it in .NET Framework and third-party libraries and tools.
  4. Hard for maintenance and debugging - Really? I wonder what O/RM has he been using. NHibernate uses log4net for logging and has option to log ALL generated SQL code. If that's not enough for you, it's Open Source, so you can attach the debugger to it and step thru the code if necessary.
    Hard to maintain? Let me tell you about the big enterprise application I had worked on several years ago and still have to maintain it... We did some bad design decisions and end up with business code in both .NET application and SQL Server stored procedures. Whenever we have to change something we make a change in stored procedure, than in .NET application, then deploy it, fix bugs, etc. Those changes in two places are rather tedious. Not to mention we don't have stored procedures under source control for obvious reasons. In other, NHibernate powered application, we have to only refactor and deploy. All our code is in one place, and under source control. That's why I favor "stored procedure"-less databases lately.
    And one more thing... Ever tried Unit / Integration Tests? You'll definitely have less bugs to cope with later on.

Final words. Don't let anyone kill your desire to learn new things just because he/she had tried it once and consider it bad. Try it for yourself, at least on a small test project. Learn more, don't limit yourself to one technology / framework / tool. Oh, and take your time to learn best practices :)

That's what ALT.NET tries to teach us.