Minwell'Space

最厉害的不是掌握语言的开发人员,而是制定规则的设计人员.

导航

NHibernate Best Practices with ASP.NET, Generics, and Unit Tests

Introduction

NHibernate, like other ORM tools, has alleviated the maintenance of thousands of lines of code and stored procedures, thus allowing developers to focus more attention on the core of a project: its domain model and business logic. Even if you automatically generate your ADO.NET data-access layer using a tool such as CodeSmith or LLBLGen Pro (both great tools), NHibernate provides the flexibility in decoupling your business model from your relational model. Your database should be an implementation detail that is defined to support your domain model, not the other way around. As forums quickly fill with heated debates concerning these points, this article will not focus on proving the benefits of using an ORM tool over ADO.NET, or NHibernate over another ORM tool, but on describing best practices for integrating NHibernate into ASP.NET using well established design patterns and "lessons learned from the field".

This article assumes a good understanding of C# and NHibernate, experience with the Data Access Object pattern, and at least a basic familiarity with Generics. If you're just getting acquainted with NHibernate, I'd recommend reading these two great introductions at TheServerSide.net: Part 1 and Part 2. For an extensive overview of the Data Access Object (DAO) design pattern, go to J2EE's BluePrints catalog.

In building solid data integration within an ASP.NET 2.0 application, we aim to achieve the following objectives:

  • Presentation and business logic layers should be in blissful ignorance of how they communicate with the database. You should be able to modify your means of data communication with minimal modification to these layers.
  • Business logic should be easily testable without depending on a live database.
  • NHibernate features, such as lazy-loading, should be available throughout the entire page life-cycle.
  • .NET 2.0 Generics should be leveraged to alleviate duplicated code.

A sample application has been included that demonstrates the merging of NHibernate, ASP.NET, and .NET Generics while meeting the above objectives. What follows is a description of how each of the aforementioned objectives has been tackled in the application. But before getting into the implementation details, let's skip right to the chase and get the sample up and running.

The Sample Application

The sample application, at the risk of being terribly cliché, utilizes the Northwind database within SQL Server 2000 to view and update a listing of Northwind customers. To demonstrate the use of lazy-loading, the application also displays the orders that each customer has made. All you need to run the sample locally is IIS with the .NET 2.0 Framework installed, and SQL Server 2000 containing the Northwind database.

To get the application up and running:

  1. Unzip the sample application to the folder of your choice.
  2. Open NHibernateSample.Web/web.config and NHibernateSample.Tests/App.config to modify the database connection strings to connect to a Northwind database on SQL Server 2000.
  3. Create a new virtual directory within IIS. The alias should be NHibernateSample, and the directory should point to the NHibernateSample.Web folder created after unzipping the application.
  4. Open your web browser to http://localhost/NHibernateSample/ViewCustomers.aspx, and you're off and running!

Now that you're able to follow along with the example in front of you, we'll examine how the application was developed to meet our design objectives...

Meeting the Design Objectives

When writing an ASP.NET application, my primary goals are:

  • Write code once and only once. (Hey, doesn't that sound like XP?)
  • Focus on simplicity and readability.
  • Keep coupling and dependencies to a minimum.

To assist with keeping logical tiers loosely coupled, the included sample application is split into four projects: Web, Core, Data, and Tests. In peeling the layers of the onion, let's begin with the simple presentation/controller layer and work our way down.

NHibernateSample.Web

As expected, the NHibernateSample.Web project contains application configuration and web pages. In this sample, the code-behind pages act as controllers, communicating with the business and data access layers accordingly. Arguably, this is not best-practice MVC separation, but it's simple, and serves well for the demonstration. (I'll leave the thoroughly debated MVC-and-ASP.NET discussion for another day.)

Here's a closer look at some of the more interesting bits...

Open Session in View

If you want to leverage NHibernate's lazy-loading (which you most definitely will), then the Open-Session-in-View pattern is the way to go. ("Session" in this context is the NHibernate session...not the ASP.NET "Session" object.) Essentially, this pattern suggests that one NHibernate session be opened per HTTP request. Although session management within the ASP.NET page life-cycle is clear in theory, it varies widely in available implementation approaches. The approach I've taken is to create a dedicated HTTPModule to handle the details of the pattern. Aside from centralizing session management responsibilities, this approach provides the additional benefit that we may implement the Open-Session-in-View pattern without putting any session management code into our code-behind pages.

To see how this has been implemented, take a look at the class NHibernateSessionModule under the App_Code directory. The following section is included in the web.config to activate the HTTPModule:

<httpModules>
<add name="NHibernateSessionModule"
type="NHibernateSample.Web.NHibernateSessionModule" />
</httpModules>

The HTTPModule included in the sample application begins a transaction at the beginning of the web request, and commits/closes it at the end of the request. There's very little overhead associated with this as NHibernate doesn't actually open a database connection until needed. As we'll see, you may switch out this strategy with others exposed by the session manager. Other strategies that you may want to consider are opening a session not associated with a transaction and/or registering an IInterceptor with the NHibernate session. (Using an IInterceptor is great for auditing purposes. See Hibernate in Action, section 8.3.2 - Audit Logging, for more details.)

NHibernate Settings Within web.config

There are two key settings within web.config to optimize NHibernate: hibernate.connection.isolation and hibernate.default_schema. By default, NHibernate uses IsolationLevel.Unspecified as its database isolation level. In other words, NHibernate leaves it up to the ADO.NET provider to determine what the isolation level is by default. If the provider you're using has a default isolation level of Serializable, this is a very strict level of isolation that can be overkill for most application scenarios. A more reasonable setting to start with is ReadCommitted. With this setting, "reading transactions" don't block other transactions from accessing a row. However, an uncommitted "writing transaction" blocks all other transactions from accessing the row. Other provider defaults include (note that they are subject to change by version):

  • SQL Server 2000 - Read Committed
  • SQL Server 2005 - Read Committed
  • Firebird - Read Committed
  • MySQL's InnoDB - Repeatable Read
  • PostgreSQL - Read Committed
  • Oracle - Read Committed

The other optional setting not to be ignored, hibernate.default_schema, is easily overlooked, but can have a significant impact on the querying performance. By default, table names within prepared NHibernate queries - such as those from CreateCriteria - are not fully qualified; e.g., Customers vs. Northwind.dbo.Customers. The crux of the problem is that sp_execsql, the stored procedure used to execute NHibernate queries, does not efficiently optimize queries unless the table names are fully qualified. Although this is a small syntactic difference, it can slow query speeds by as much as an order of magnitude in some cases. Explicitly setting hibernate.default_schema can provide as much as a 33% overall performance gain on data intensive pages. The following is an example of declaring these settings in web.config:

<add key="hibernate.connection.isolation" value="ReadCommitted" />
<add key="hibernate.default_schema" value="Northwind.dbo" />

In order to decouple the implementation details of data access from the NHibernateSample.Web project, NHibernate session management has been completely separated into the NHibernateSample.Data project. To inform NHibernate where the embedded HBM mapping files reside, define a web.config setting named HBM_ASSEMBLY. NHibernate will review this assembly for embedded HBM files to load object mappings. (This is not a setting used directly by NHibernate, but by a custom class we'll be reviewing soon.)

A Simple List and Update Form

The web project contains two web pages: ViewCustomers.aspx and EditCustomer.aspx. (I'll give you three guesses to figure out what they do.) The important thing to note is that the code-behind pages work with a DAO factory to talk to the database; i.e., the code isn't bound to a concrete implementation of a data access object. This makes it much easier to swap DAO implementations and unit test your code without depending on a live database. With everything in place, the following is an example of how easy it is to retrieve all the customers in the database:

IDaoFactory daoFactory = new NHibernateDaoFactory();
ICustomerDao customerDao = daoFactory.GetCustomerDao();
IList<Customer> allCustomers = customerDao.GetAll();

In the above example, a concrete reference to NHibernateDaoFactory is retrieved via new. In production code, this reference may (should?) be "injected" at runtime using a technique called Inversion of Control (IoC), or "Dependency Injection". Martin Fowler has written a great introduction to this pattern. Think of it as code decoupling on steroids. With IoC, it's possible to remove many direct instantiations of concrete objects along with the inflexibility that comes along with these dependencies. The many benefits of IoC include a flexible framework, an emphasis on coding-to-interface, and extreme ease of unit testing. The drawback is another layer of complexity within the application. Here are two great tools for putting IoC into practice:

  • Spring .NET: This framework provides IoC declared via XML configuration files. Spring .NET also has a number of other powerful modules, making it an attractive option if you're looking for more than just IoC. This is the framework I'm currently using for IoC, but it's not the only good choice out there.
  • Castle Windsor: The Castle Windsor Container project provides good IoC support with a combination of configuration and strongly typed declarations. Some of the advantages Castle Windsor brings to the table are less XML and more compile-time error catching. Like Spring .NET, the Castle project also provides a wide assortment of additional development utilities.

While it is acceptable to use the new keyword to create NHiberanteDaoFactory within your code-behind, or controller, your business objects should never create this dependency directly. Instead, their DAO dependency should be provided via a public setter or via a constructor. (IoC can help here as well.) This greatly enhances your ability to unit test with "mock" DAO classes. For example, the following code, found within NHibernateSample.Tests.Data.CustomerDaoTests, retrieves a customer and gives it its DAO dependency:

IDaoFactory daoFactory = new NHibernateDaoFactory();
ICustomerDao customerDao = daoFactory.GetCustomerDao();
Customer customer = customerDao.GetById("CHOPS", false);
// Give the customer its DAO dependency via a public setter
customer.OrderDao = daoFactory.GetOrderDao();

Using this technique, the business layer never needs to depend directly on the data layer. Instead, it depends on interfaces defined within the same layer, as we'll see next within the NHibernateSample.Core project.

NHibernateSample.Core

The NHibernateSample.Core project contains the domain model and NHibernate HBM files. This project also contains interfaces to data access objects in the NHibernateSample.Core.Data namespace. (Arguably, the HBM files belong in the NHibernateSample.Data project, but the maintenance convenience of having the HBM files physically close to the domain objects they describe far outweighs this break in encapsulation.)

Data Dependency Inversion

You'll notice that the NHibernateSample.Core project does not contain implementation details of data access objects, only interfaces describing the services it needs. The concrete DAO classes which implement these interfaces are found within NHibernateSample.Data. This is a technique called Separated Interface by Martin Fowler or "Dependency Inversion" by Robert Martin in Agile Software Development. Considering NHibernateSample.Core as an "upper-level layer" and NHibernateSample.Data as a "lower-level layer", then, as Martin describes, "each of the upper-level layers declares an abstract interface for the services that it needs. The lower-level layers are then realized from these abstract interfaces. ... Thus, the upper layers do not depend on the lower layers. Instead, the lower layers depend on abstract service interfaces declared in the upper layers". Dependency inversion is the perfect technique for breaking a bi-directional dependency between domain and data layers.

To see this in action, the data interfaces are described in NHibernateSample.Core.DataInterfaces. IGenericDao is a generic interface for providing typical data access functionality. IDaoFactory then acts as an interface for one or more DAO factory classes. Coding to the IDaoFactory interface allows you to create one concrete DAO factory for production code, and another concrete DAO factory for returning "mock" DAO objects for unit testing purposes. (This is described by the abstract factory pattern.) Leveraging mock objects in unit tests provides a means for testing a single responsibility at a time.

Generics with NHibernate

By far, one of the greatest benefits that C# 2.0 has brought to the table is the inclusion of generics. With generics, more code reuse can be effectively realized while still enforcing strongly typed coding "contracts". But while the benefits are great, NHibernate has not yet been upgraded to take advantage of this new language feature. (I know they're busy at work doing just that, though.) In the meantime, a solution can be found here called - quick, take a guess - NHibernate.Generics. NHibernate.Generics provides generic-typed wrappers for the IList and ISet collections that NHibernate currently binds to. I won't spend much time describing the approach, as Oren Eini's website will do a much more thorough job of this, but there is one particular note of interest in the code. Within the constructors of the NHibernateSample.Core.Domain objects, Customer and Order are "wire up" code for handling relational scaffolding. The collection wrappers accept two anonymous methods for handling add/remove events from either end of the relationship. So if you remove a child from a parent, the parent automatically gets removed from the child, and vice versa. (Look at NHibernateSample.Tests.CustomerTests to see it work.)

public Customer() {
// Implement parent/child relationship add/remove scaffolding
_orders = new EntityList<Order>(
delegate(Order order) { order.ParentCustomer = this; },
delegate(Order order) { order.ParentCustomer = null; }
);
}

In the above example, note that private members encapsulating these relationships must be in the following format: _camelCase. There are a couple other caveats on the NHiberante.Generics website, but, overall, they're a small price to pay for having support for NHibernate generics. But be sure to keep an eye on NHibernate's website as an upgrade supporting generics is in the works.

NHibernateSample.Data

The NHibernateSample.Data project contains the implementation details for communicating with the database and managing NHibernate sessions.

The DAO Factory and Generic DAO

The DAO factory and generic DAO objects I've implemented as NHibernateDaoFactory and GenericNHibernateDAO, respectively, are C# ports of the Java versions described in detail at NHibernate's website. I highly recommend reviewing this article in detail. The most impressive thing to note is that it takes just a few lines of code to create a full-blown DAO object ready for use:

  1. Add a new inline implementation and retrieval method for the DAO, to NHibernateSample.Data.NHibernateDaoFactory.
  2. Add a new inline interface and retrieval method to NHibernateSample.Core.DataInterfaces.IDaoFactory.

Looking at the ICustomerDao example already in the sample application, that took about five lines of code...not too bad.

Handling the NHibernate Session

Finally, the only remaining question is how are NHibernate sessions managed? Details answering this question may be found in the class NHibernateSessionManager. Within this class, the basic flow for retrieving a session is as follows:

  1. The client code calls NHibernateSessionManager.Instance.GetSession().
  2. If not already instantiated, this singleton object builds the session factory, loading HBM mapping files from the HBM_ASSEMBLY declared in web.config. (NHibernateSessionManager is a singleton since building the session factory is very expensive.)
  3. GetSession looks to see if a session is already bound to System.Runtime.Remoting.Messaging.CallContext["THREAD_SESSION"].
  4. If an open NHibernate session is not found, then a new one is opened (and bound to an optional IInterceptor) and then set to CallContext["THREAD_SESSION"].
  5. GetSession then returns the session assigned to CallContext["THREAD_SESSION"].

This flow, as well as the rest of NHibernateSessionManager, follows that closely described in Hibernate in Action, chapter 8 - Writing Hibernate Applications.

The HTTPModule described in the NHibernateSample.Web project opens a transaction at the beginning of a web request, and commits/closes it at the end of the request. The following is an example of modifying the HTTPModule so that an IInterceptor gets bound to the session as well as being contained within a transaction:

public void Init(HttpApplication context) {
context.BeginRequest +=
new EventHandler(InitNHibernateSession);
...
}
private void InitNHibernateSession(object sender, EventArgs e) {
IInterceptor myNHibernateInterceptor = ...
// Bind the interceptor to the session.
// Using open-session-in-view, an interceptor 
// cannot be bound to an already opened session,
// so this must be our very first step.
NHibernateSessionManager.Instance.RegisterInterceptor(myNHibernateInterceptor);
// Encapsulate the already opened session within a transaction
NHibernateSessionManager.Instance.BeginTransaction();
}

NHibernateSample.Tests

I'll assume you can probably guess what this project is for.

Unit Test Performance

It's imperative for unit tests to be blazing fast. If a suite of unit tests takes too long to run, developers stop running them - and we want them running unit tests all the time! In fact, if a test takes more than 0.1 second to run, the test is probably too slow. Now, if you've done unit testing in the past, you know that any unit test requiring access to a live database takes much longer than this to run. With NUnit, you can put tests into categories, making it easy to run different groups of tests at a time, thus excluding the tests that connect to a database most of the time. Here's a quick example:

[TestFixture]
[Category("NHibernate Tests")]
public class SomeTests
{
[Test]
public void TestSomethingThatDependsOnDb() { ... }
}

Testing with NHibernate

In a previous version of this article, the ISession was stored and retrieved via HttpContext.Current.Items. The problem with this is that it forced you to simulate an HTTP context when running unit tests. It also prevented the framework from being easily ported to a Windows application. Instead, it is recommended that the ISession be stored and retrieved via System.Runtime.Remoting.Messaging.CallContext. (Thanks to Tadeu Camargo da Silva, on the NHibernate forums, and the feedback below for recommending this solution.) Using CallContext provides proper storage of the ISession for Web apps, Windows apps, and unit tests, alike. Take a look at NHibernateSample.Tests.Data.CustomerDaoTests for a unit test that is now "HTTP agnostic." Additionally, as you'll see in the unit test, unless you want data changes made within your tests to be committed to the database, it's a good idea to rollback the transaction.

Testing with NHibernate "Mocked"

Unless you're specifically testing DAO classes, you usually don't want to run unit tests that are dependent on a live database. They're slow and volatile by nature; i.e., if the data changes, the tests break. When testing business logic, unit tests shouldn't break if data changes. But the major obstacle is that business objects themselves may depend on DAOs. Using the abstract factory pattern that we've put into place, we can inject mock DAOs into the business objects, thereby simulating communications with the database. An example is included in NHibernateSample.Tests.Domain.CustomerTests. The following snippet creates the mock DAO and gives it to the Customer object via a public setter. Since the setter only expects an object implementing the IOrderDao interface, the mock object can easily replace all of the live-database behavior.

Customer customer = new Customer("Acme Anvils");
customer.ID = "ACME";
customer.OrderDao = new MockOrderDao();

Unfortunately, more often than not, you're maintaining legacy code that has no semblance of the ideal "code-to-interface" that allows for such flexibility. Usually, there are many explicit dependencies to concrete objects, and it is difficult to replace data-access objects with mock objects to simulate database communications. In these situations, your options are to either refactor the legacy code to fit within a test harness, or to use an object mocking tool such as TypeMock. With TypeMock, it is even possible to mock sealed and singleton classes - an impressive feat to pull off without such a tool. Albeit powerful, TypeMock is best left alone unless absolutely needed; using TypeMock prematurely makes it tempting to not always code to interface. The more appropriate course to take when working with legacy code - time and budget permitting - is to refactor the code for greater flexibility. Working Effectively with Legacy Code by Michael Feathers is full of great ideas for refactoring legacy code into a test harness.

Putting it into Practice

The sample application provides a strong data-layer foundation for building scalable web applications up to the enterprise level. (Almost all of the techniques also fit well for building Windows Forms applications.) But before using it within your own environment, I would recommend the following:

  • Place NHibernateSample.Core.DataInterfaces.IGenericDAO, NHibernateSample.Data.GenericNHibernateDAO, and NHibernateSample.Data.NHibernateSessionManager into a reusable class library as they would be transferable between applications with no code modifications whatsoever.
  • Select an IoC provider to inject DAO dependencies into your page controllers.
  • Finally, I would ignore how I've setup my ASPX code-behind logic - it can quickly create monolithic code-behind pages; instead, I would consider using patterns such as Model-View-Presenter (my personal favorite), Page Controller, Front Controller, or other MVC-like approaches. But again, that's for a completely different discussion altogether!

I hope this article has helped with putting best practices into place for leveraging the benefits of ASP.NET, NHibernate, and Generics. I'm currently having great success with this approach on my projects, and look forward to hearing your experiences as well. Please let me know if you have any questions or suggestions.

posted on 2006-07-25 18:50  Minwell  阅读(1047)  评论(0编辑  收藏  举报