• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

[Reference]NHibernateBestPracticeswithASP.NET,1.2ndEd.

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

http://www.codeproject.com/aspnet/NHibernateBestPractices.asp

Preface to the 1.2nd Edition

In March of 2006 I published my initial thoughts on NHibernate best practices with ASP.NET, generics and unit tests. I've been delighted to learn that these ideas have been implemented in a number of real-world scenarios with strong success. Since then, I've worked with many people to refine these ideas, learn from mistakes and leverage a more powerful version of NHibernate. Accordingly, although only a modest, yet important, amount of modification has been made to the underlying architecture, some other important factors have been updated and addressed in this article:

  • Quite simply, NHibernate is awesome. In the previous edition of this article, I assumed you already knew this...but I now try to convince the dissenters as well.
  • NHibernate 1.2 natively supports generics.
  • NHibernate 1.2 natively supports nullable types.
  • NHibernate 1.2 natively supports mapping attributes.
  • NHibernate 1.2 can communicate with stored procedures.
  • Using CallContext for ISession storage in ASP.NET was susceptible to failure under load.
  • Exposing a publicly settable ID property created a point-of-susceptibility.
  • Providing automatic parent/child wiring, via Ayende's very helpful NHibernate.Generics, was more headache than help.
  • Have you used Rhino Mocks 3.0, NUnitAsp, or Fit? Well, these are all discussed with an expanded emphasis on test-driven development.
  • As an alternative to my recommendations, also consider Castle Project's offerings such as MonoRail and/or ActiveRecord for a simple yet powerful out-of-the-box framework for ASP.NET. In fact, if it's technically feasible and you can generate buy-in on your team for these off-the-shelf tools, I would recommend using them over a ground-up solution. But even if you do use Castle Project facilities, this article should still have a lot of useful information for you!
  • In addition to those listed above, there are other important refactorings and fixes throughout the article and the code. This edition is by no means just a light touch-up of the original article.
  • In addition to an overhaul of the original sample code, an expanded "enterprise" sample has been included demonstrating:
    • NHibernate with web services
    • NHibernate with multiple databases
    • Integration with Castle Windsor
    • A reusable data layer for the data access components.
    • A simple example of Model-View-Presenter

A quick thanks goes out to those who have implemented my ideas in their own work and have given plenty of ideas for improvement and consideration! Now onto the 1.2nd edition...

Article Contents

Introduction

Why Use an ORM?

[Author's Note: The following is an excerpt taken from a book I tinker with in my "spare" time.]

"As we look to the horizon of a decade hence, we see no silver bullet. There is no single development, in either technology or management technique, which by itself promises even one order-of-magnitude improvement within a decade in productivity, in reliability, in simplicity."

These prophetic words were put forth by Frederick Brooks' in his now legendary paper, No Silver Bullet, in 1986. Heeding Brooks' words to a tee, it wasn't until more than a decade later, in 1997, that the software world was presented with a hint of an upcoming silver bullet in the form of NeXT's Enterprise Object Framework...one of the first object-relational mappers (ORM). Although not regularly conspicuously stated – often to avoid being seen as a heretic of software dogma – it is commonly accepted by many that ORM technologies, when used correctly, are, in fact, a silver bullet for software development. With the maturation of NHibernate, the ORM silver bullet has been formally introduced to the world of .NET.

The most common dissenters of ORM technologies, in general, are developers using Microsoft technologies. (As I've placed myself squarely into this realm for the past decade or so, I feel quite comfortable bringing us up first!) There seems to be an unwritten rule that "if it wasn't invented by Microsoft, then wait until Microsoft puts out the right way to do it." Stepping up to the plate, Microsoft intends to do just that with "LINQ to Entities" in the upcoming C# 3.0. (Yes, officially discard the use of "ObjectSpaces" and "DLINQ.") Developers may continue to wait for this technology or, alternatively, start realizing the benefits of ORM immediately. To be fair, the not-invented-by-Microsoft toolset for .NET developers used to be sparse but has been steadily growing since the advent of .NET. Circa 2004, the "not created by the mothership" toolset of open source tools finally began to reach a respectable level of maturity and should be seriously considered for any .NET endeavor. (And since, statistically, the majority of software projects fail, it sounds like the consideration of an expanded toolset is certainly warranted.) The impending introduction of LINQ certainly brings great benefits to flexible querying. Fortunately, LINQ is not exclusive to Microsoft data-access layers and LINQ for NHibernate is already well underway by Ayende Rahien.

Other dissenters of these technologies suggest that ORMs kill application performance and only provide a significant improvement to productivity in the initial stages of development. Furthermore, the argument continues, is that the use of an ORM becomes a serious detriment to project success only later in the project, when issues of performance and maintainability begin to have a more noticeable effect. Three obvious retorts come to mind in response to these protests.

First and foremost, in support of ORMs, using a framework such as NHibernate increases your performance as a developer. If you can spend 90% less time (yes, I said "90% less time") on developing data access code, then quality time can be spent improving the domain model and tuning performance – assuming it becomes necessary. Furthermore, leveraging a simple profiling tool goes a long way towards implicating the 5% of code that's causing 95% of the performance bottleneck. And in the cases in which the data access layer is the bottleneck, simple adjustments can usually be made to reap substantial improvements. Incidentally, this is no different than when not using an ORM. (Be sure to check out Peter Weissbrod's introductory article to profiling NHibernate applications.) And in the very few situations in which the ORM framework is the bottleneck and can't be adjusted for improvement, it's trivially simple to bypass the ORM altogether if the data access layer has been properly abstracted.

Secondly, NHibernate, specifically, provides an incredible amount of control over all aspects of data-loading behavior. This has positive effects on developer productivity, application scalability, and application stability. Caching is certainly available – but this is readily available in non-ORM solutions as well. Other out-of-the-box features include lazy loading, support for inheritance, declaration of immutable classes, loading of read-only properties, support for generics, stored procedures, blah blah blah. The point is that all these powerful features have been proven in real-world scenarios and, therefore, have removed many hours of developing, debugging and tweaking a custom data access layer. (And if you really feel the need to get into the code, that's just fine since NHibernate's an open source project.)

Finally, I would argue that those who feel that ORMs like NHibernate become maintenance headaches later in a project are working with a coding architecture that would inhibit the maintenance of any data access layer. Just because I've suggested that NHibernate is a silver bullet doesn't imply that it eliminates the need for proper application design. With the proper amount of judicious, architectural forethought, NHibernate is quite possibly the most maintainable solution for tying a .NET application to a database.

Needless to say, 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, NHibernate provides the flexibility in decoupling your domain 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. The remainder of this article focuses on describing best practices for integrating NHibernate into ASP.NET using well established design patterns and "lessons learned from the field".

Goals and Overview of Article

This article assumes a good understanding of C# and NHibernate, knowledge of the Data Access Object / Repository pattern, and at least a basic familiarity with Generics. Note that this article does not focus on using NHibernate but, instead, on integrating NHibernate into ASP.NET applications. If you're just getting acquainted with NHibernate, I'd recommend reading these two great introductions at TheServerSide.net: Part 1 and Part 2. (Also keep an eye out for Pierre Kuate's forthcoming NHibernate in Action.) For an extensive overview of the Data Access Object pattern, which is leveraged heavily within the samples, go to J2EE's BluePrints catalog. Although I use the phrase "Data Access Object" (or "DAO") throughout, it is interchangeable with Eric Evans' "Repository" in Domain-Driven Design. I just find "DAO" more convenient to type!

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

  • Presentation and domain layers should be in relative 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 ASPX page life-cycle.
  • .NET 2.0 Generics should be leveraged to alleviate duplicated code.

Two sample applications have been included, demonstrating the use of NHibernate with ASP.NET while meeting the above objectives:

  • Basic NHibernate Sample: This sample demonstrates the fundamentals of using NHibernate with ASP.NET and unit tests with a simple-to-understand, but not completely reusable, architecture.
  • "Enterprise" NHibernate Sample: This sample is provided with an architecturally sound grounding using proven design patterns which should allow you to quickly reuse the framework in almost any sized ASP.NET project. This sample also serves to demonstrate NHibernate with ASP.NET and "a whole bunch of other stuff" including communicating with multiple databases, using the pattern Model-View-Presenter, setting up a simple web-service which uses NHibernate, and integrating with Castle Windsor. Although code is included for communicating with multiple databases, a detailed explanation is beyond the scope of this article and may be found at the CodeProject.com article, Using NHibernate with Multiple Databases. (Note that that article's sample application is compatible with NHibernate 1.0x; albeit, the general approach is still applicable.)

What follows now is a description of how each of the aforementioned design objectives has been tackled in the sample applications. But before getting into the implementation details, let's skip right to the chase and get the sample up and running.

Running the Basic NHibernate Sample

The sample application, at the risk of being terribly cliché, utilize the Northwind database within SQL Server 2005 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 samples locally is IIS with the .NET 2.0 Framework installed, and SQL Server 2005 (or 2000) containing the Northwind database. (Since SQL Server 2005 doesn't come with Northwind by default, you can simply backup the Northwind DB from 2000 and restore it into 2005.) The samples also port to SQL Server 2000...simply modify the NHibernate configuration settings, accordingly.

To get the basic NHibernate sample application up and running:

  1. Unzip the sample application to the folder of your choice.
  2. Create a new virtual directory within IIS. The alias should be BasicNHibernateSample, and the directory should point to the BasicSample.Web folder that was created after unzipping the application.
  3. Open BasicSample.Web/web.config and BasicSample.Tests/App.config to modify the database connection strings to connect to a Northwind database on Microsoft SQL Server.
  4. If, and only if, you're running IIS 7, modify web.config by commenting out the "compatible with IIS 6" section and uncomment the "compatible with IIS 7" section.
  5. Open your web browser to http://localhost/BasicNHibernateSample/Default.aspx, and you're off and running!

Steps for getting the "enterprise" sample up and running are discussed in Extending the Basics to an "Enterprise" Solution. But before that, now that you're able to follow along with the basic sample in front of you, we'll examine how the application was developed to meet our design objectives...

NHibernate Integration Basics

When developing an application, my primary goals are to:

  • Write code once and only once.
  • Focus on simplicity and readability.
  • Keep coupling and dependencies to a minimum.
  • Maintain a clean separation of concerns.
  • Do all of the above using test-driven development.

This section discusses using these design principles for the integration of NHibernate into ASP.NET applications. We'll do this by dissecting the internals of the Basic NHibernate Sample.

Architectural Notes

The basic sample should not, necessarily, be seen as a reusable framework for your own ASP.NET application. The focus within this example application is on presenting NHibernate integration in a simple and concise manner. If you are looking for a "ready for the real-world" architecture, be sure to take a look at Extending the Basics to an "Enterprise" Solution after reviewing this section. With that said, the basic sample does present a number of best practice techniques and design patterns:

Separated Interface

Separated Interface, also known as Dependency Inversion, is a technique for establishing a clean separation of concerns between application tiers. This technique is described by Martin Fowler, by Object Mentor, and in further detail in Robert Martin's Agile Software Development. The technique is often used for cleanly separating a data access layer from a domain layer. For example, assume a Customer object - in the domain layer - needs to communicate with a data access object (DAO) to retrieve a number of past Orders. (Whether or not the domain layer should ever communicate directly with a DOA is left for another discussion.) Consequently, the Customer object has a dependency on the Order DAO - in the data layer. But for the Order DAO to return orders, the DAO requires a dependency back to the domain layer.

The simplest solution is to put the data layer, containing the DAOs, into the same physical assembly as the domain layer. To maintain a "virtual" separation of concerns, the containing project could include two folders, one named Domain and the other named Data. (I've actually used this approach on a good-sized project, in a former life, with regrettable consequences.) This approach brings with it a number of ill effects:

  • The domain and data layers share a bi-directional dependency with each other.
  • There's nothing to prevent the data layer from bleeding into the domain layer and vice-versa. If it can't be structurally prevented then it will occur.
  • It's difficult to unit test the domain layer without using a live database to support the data layer. Among other drawbacks, this brings unit testing performance to a crawl; therefore, developers stop unit testing.

Alternatively, the domain and data layers could be placed into physically separate assemblies; e.g., Project.Core and Project.Data, respectively. The domain layer (the Project.Core assembly) would contain domain objects and DAO interfaces. The data layer (the Project.Data assembly) would contain concrete implementations of the DAO interfaces defined in the domain layer. This is shown in the package diagram below. Note that the arrow signifies a uni-directional dependency from the data layer to the domain layer.


This clean separation brings with it a number of benefits:

  • Developers are structurally unable to put concrete data-access code into the domain layer without adding a number of easy-to-spot references, such as to NHibernate or System.Data.SqlClient.
  • The domain layer remains in ignorant bliss of how the data layer communicates or with who it communicates. Therefore, it's easier to switch out the data-access implementation details (e.g., from ADO.NET to NHibernate) without having to modify the domain layer.
  • Having dependencies on interfaces makes it easy to provide a "mocked" data-access layer to the domain layer while unit testing. This keeps unit tests blazing fast and eliminates the need to maintain test data in the database.

An implementation example of Separated Interface is included in the sample applications and is discussed further below.

Dependency Injection

Separated Interface, as described above, introduces a dilemma: how are the concrete DAO implementations given to the domain layer which only "knows" about interfaces? Dependency Injection, also known as Inversion of Control (IoC), describes the technique for doing this. With Dependency Injection, it's possible to remove many direct instantiations of concrete objects along with the inflexibility that comes along with calling the new keyword directly. Dependency Injection may be performed manually or via an IoC Container. The manual approach is performed by simply passing an object's "service dependencies" to it via its constructor or via property setters. I've written an introduction to this approach within the CodeProject article, Dependency Injection for Loose Coupling. (Note that a "service dependency" may be a DAO, an emailing utility, or anything that leverages external, expensive, or shouldn't-be-used-in-unit-test resources.) This manual approach is most useful within unit tests because the explicitness of passing concrete dependencies is helpful in describing functionality. (Martin Fowler has some wise words on the value of being explicit.) Alternatively, dependency injection may be performed via an IoC Container driven by code or XML. (Fowler has also written a great introduction to IoC Containers, service locators and making an appropriate choice between the two.) Think of IoC Containers as providing decoupling on steroids. The benefits of an IoC Container include a flexible, loosely-coupled framework and an increased emphasis on coding-to-interface. The drawback is an added layer of complexity within the application. This is one of the many trade-offs between flexibility and complexity which needs to be considered when developing an application. Here are two great tools for putting IoC Containers into practice:

  • Castle Windsor: The Castle Windsor, IoC Container provides great IoC support using a combination of XML configuration and strongly typed declarations. Some of the advantages Castle Windsor brings to the table are less XML and more compile-time error catching, when compared against other options. The Castle Project also has a number of other powerful modules, making it an attractive option if you're looking for more than just IoC. Wide support has been shown for this IoC Container which has generated a lot of buzz around the .NET development community. The "Enterprise" NHibernate Sample includes an example of using Castle Windsor.
  • Spring .NET: This framework provides IoC declared via XML configuration files. Like the Castle Project, Spring.NET also provides a wide assortment of additional development utilities. This option should be particularly attractive for developers coming from the Java world and are already familiar with the Spring Framework.

I've found that using an IoC Container is most useful outside of unit tests for enabling ASPX pages to be given dependencies, to avoid ever having to call the new keyword directly. This may be taken further to wire up dependencies within a presenters-layer or service layer, as well.

Design by Contract

Design-by-Contract (DBC) is quite simply the best way to never have to use the debugger again. Although infrequently discussed, this technique should be given the same amount of praise that test-driven development receives. (Not that I'm saying that DBC gets jealous, but it should.) It increases quality, reduces checks for null, reduces debugging time, and greatly improves the overall stability of an application. To be more technically descriptive, DBC is a technique for contractually obligating users of your code (which is usually you, yourself) to use methods and properties in a particular way and for those methods and properties to promise to successfully carry out the given request. If the contract is not followed, an exception is thrown. It may seem a bit drastic at first, but it goes a long way to improving code quality. With DBC, "pre-conditions" assert what contractual obligations must be adhered to when invoking a method or property. "Post-conditions" assert what contractual obligations have been ensured. I highly recommend reading an introduction to Design-by-Contract; you'll be hooked on using it very quickly. The sample projects included with this article use a modified DBC class originally written by Kevin McFarlane. The original allows conditional compilation for turning contracts on for debug mode and off for release mode while the variation included in this article's samples maintains contractual obligations regardless of compilation mode. In my opinion, a contract is always a contract and the behavior should never vary.

In the code, you'll find that pre-conditions are declared with Check.Require while post-condition are declared with Check.Ensure. As DBC is not domain-specific, this class is appropriately pulled out into a reusable, utilities library in the "enterprise" sample.

The Basic Sample Application

So what are we working with here? The basic application fulfills the following user stories:

  • User may view listing of suppliers and their products.
  • User may view listing of existing customers.
  • User may view details of customer.
  • User may view a listing of past orders placed by a customer.
  • User may view a listing of order summaries, including product name and total quantity, placed by a customer.
  • User may edit customer details.
  • User may add a new customer.

Regardless of how valuable this application may or may not be, the above user stories are enough to demonstrate the primary aspects of NHibernate integration. To assist with keeping logical tiers loosely coupled, the included sample application is split into four projects: Tests, Web, Core and Data. As a policy, I use <ProjectName>.<LayerName> for naming projects; e.g., BasicSample.Data. Although this simple separation of concerns will work for now, a more realistic architecture will be discussed later. In peeling the layers of the onion, let's begin with the testing layer and work our way towards the simple presentation layer.

BasicSample.Tests with NUnit, NUnitAsp, Rhino Mocks and Fit

I'll assume you can probably guess what this project is for. In the first edition of this article, unit testing was only lightly discussed. As industry, and personal, experience has proven, test-driven development is a pivotal factor in producing high quality products which are more adaptable to change. Furthermore, taking a test-driven approach tends to produce better designs and, as a side effect, creates a lot of perfectly valid technical documentation. For a terrific beginner's introduction to a "day in the life" of test-driven development, take a look at Kent Beck's Test-Driven Development: By Example. Examining the unit tests of the sample application provides an overview of how the application is structured and what functionality is available. After taking a look at the unit tests, we'll delve further into the code they test.

A Note on 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. But at the very least, these "slow" tests should be run every night within a Continuous Integration environment. Here's a quick example of categorizing an NUnit, unit test:

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

Domain Layer Unit Tests

For simplicity, the domain layer of this application is light, to say the least. But even in the lightest of domain layers, it's important to have, at a minimum, every non-exception path covered by a unit test. The domain layer tests may be found in the namespace BasicSample.Tests.Domain. (As a side, is it overkill that CustomerTests.CanCreateCustomer tests the getter/setter of almost every property? It's startling how many trivial bugs you catch in the process.) While reviewing the tests, you may notice the class DomainObjectIdSetter.cs; the motivations for this class will be discussed later.

To run the unit tests, open NUnit, go to File/Open Project and open BasicSample.Tests/bin/Debug/BasicSample.Tests.dll. To prevent the more time-consuming tests from running, go to the Categories tab within NUnit and double-click both "Database Tests" and "Web Smoke Tests." Additionally, click "Exclude these categories" at the bottom. Now, when you run the unit tests, only the domain logic tests will run and not be slowed down by HTTP and database access tests. For such a small application, the added overhead of the "slow" tests is almost negligible, but can add many minutes to running the unit tests of larger apps.

Using Test Doubles for the Data Access Layer

Before getting into simulating the database layer, it should be noted that a nomenclature exists for describing different types of simulated services. Dummies, fakes, stubs and mocks are all used to describe different types of simulated behavior. An overview of the differences highlights a few which will be included in Gerard Meszaros' upcoming XUnit Test Patterns. Meszaros offers "test double" to generically describe any of these behaviors. Stubs and mocks are two such test doubles demonstrated in the sample code.

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. And when testing domain logic, unit tests shouldn't break if the database changes. But the major obstacle is that domain objects themselves may depend on DAOs. Using the abstract factory pattern that is in place in the sample (discussed later), and the associated DAO interfaces, we can inject DAO test doubles into the domain objects, thereby simulating communications with the database. An example is included in the test CustomerTests.CanGetOrdersOrderedOnDateUsingStubbedDao. The following snippet, from the unit test, creates the DAO stub and injects it into customer via a public setter. Since the setter only expects an implementation of the IOrderDao interface, the stub DAO easily replaces all of the live-database behavior.

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

As an alternative to writing DAO stubs, which are generally static by nature and often amount to quite a bit of "not implemented" code, it is also possible to mock the DAO using a tool such as Rhino Mocks or NMock. Either selection works perfectly well, but Rhino Mocks invokes methods in a strongly typed manner as opposed to using strings, as NMock does. This makes its usage compile-time checked and assists with renaming properties and methods. The test CustomerTests.CanGetOrdersOrderedOnDateUsingMockedDao demonstrates using Rhino Mocks 3.0 to create a mocked IOrderDao. Although setting up a mocked object appears more complicated than setting up a stub, the added flexibility and greatly reduced "not implemented" code are convincing benefits. The following code, found in the class MockOrderDaoFactory.cs, shows how IOrderDao is mocked with Rhino Mocks. It essentially creates a "static" mock, or stub, in the fact that it doesn't matter what is passed in for arguments; it'll always return the same example orders created by TestOrdersFactory. But mocking with Rhino Mocks isn't limited to dumb reflexes, such as this, and can be as variably responsive as required.

public IOrderDao CreateMockOrderDao() {
MockRepository mocks = new MockRepository();
IOrderDao mockedOrderDao = mocks.CreateMock<IOrderDao>();
Expect.Call(mockedOrderDao.GetByExample(null)).IgnoreArguments()
.Return(new TestOrdersFactory().CreateOrders());
mocks.Replay(mockedOrderDao);
return mockedOrderDao;
}

Unfortunately, more often than not, you're maintaining legacy code that has no semblance of the ideal "code-to-interface" that allows for such test-double injection. Usually, there are many explicit dependencies to concrete objects, and it is difficult to replace data-access objects with test doubles to simulate a live database. 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 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.

Unit Testing NHibernate DAOs

In the previous edition of this article, NHibernate's ISession was stored and retrieved solely via System.Runtime.Remoting.Messaging.CallContext. Although perfectly fine for WinForms and NUnit tests, this is a very bad idea for ASP.NET as the ISession may be "lost" under load. (These two articles provide further explanation for why using CallContext in ASP.NET applications is a bad idea.) For proper integration with ASP.NET applications, the ISession should be stored in HttpContext.Current.Items; but doing so forces you to simulate an HTTP context when running unit tests. It also prevents the framework from easily porting to WinForms. A better approach is to use the proper repository when appropriate. So if a web context is available, then use HttpContext; otherwise, use CallContext. Implementation details of this combined approach are discussed later. (A thanks goes out to the many people in the article comments that raised this concern.) Take a look at BasicSample.Tests/Data/CustomerDaoTests.cs for unit tests that are HTTP-agnostic with respect to how the ISession is managed. As a side, 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.

As demonstrated in the sample, it's possible to create a generic DAO that works for any persistent object. (Details of which will be discussed later.) This leads to the discussion of what should be tested and how should it be done? Should each concrete DAO be fully tested? How is the test data maintained? Personal experience suggests these guidelines:

  • Make sure a unit test exists to test each method of the generic DAO, once. For example, if you have 10 DAOs which implement the generic DAO, only one of them needs to be fully tested for each method. Unit testing the other nine implementations of the generic DAO will provide little added value.
  • Make sure a unit test exists to test each extension method to the generic DAO. For example, if you have a CustomerDao class which inherits from the generic DAO and then extend it with an additional method, such as GetActiveCustomers(), then a unit test should be written to test this extension method.
  • Make sure a unit test exists to fully test each "specialty" DAO. For instance, the DAO BasicSample.Data/HistoricalOrderSummaryDao.cs does not inherit from the generic DAO and would be considered a specialty DAO. Therefore, a unit test exists to test each of its methods.
  • Use a tool such as NDbUnit to put the test database into a known state both before and after DAO unit tests are run.
  • Always remember, unit tests should never depend on the actions of another unit test! They should be independent and able to be run in isolation. E.g., a delete-test should not depend on the previous insert-test having successfully completed. Note that TestFixtureSetUp, and other setup/teardown methods, are OK to run while still being able to consider the test to have run in isolation.

Using Fit Test Doubles for the Presentation Layer

When the domain layer was tested, test doubles were used to simulate NHibernate communications with the database. Similarly, it's handy at times to use this same approach when testing the presentation layer. Suppose you're working on an ASP.NET project with a dedicated "creative" team. The creative folks, with their black turtlenecks, are in charge of developing the look and feel of the application. While they're working on the graphical layout, you shouldn't be hindered to develop a presentation layer for viewing domain-logic & data-access results and for getting client feedback, while still being able to put off decisions such as master-page setup, security enforcement, and other presentation-specific decisions. In another scenario, suppose you're working on a number of complicated business rules which you'd like the client to be able to verify without having to write a few dozen unit tests to encapsulate each minor variation. FIT (Framework for Integrated Test) is a tool for developers to fake the presentation layer very quickly and provide for a more collaborative effort between developers and project stakeholders. As the Fit site states, this is done "to learn what the software should do and what it does do. It automatically compares customers' expectations to actual results." Arguably, a tool such as this isn't "basic" and isn't required for testing NHibernate; but the importance of test-driven development needs to be emphasized and a tool such as Fit, when used appropriately, is just as applicable to software quality as NUnit.

For viewing Fit test results, you can use WinFITRunnerLite, which runs Fit tests in a Windows client similar to NUnit, or FitNesse, which provides a web-based wiki for modifying test inputs and viewing Fit test results. Although it takes a little more setup, FitNesse provides a very flexible framework for allowing clients to participate in validating coding logic and application workflow. The following screenshot shows a simple example of the output you'd expect to see from running a calculator test with FitNesse:

Although implementation examples of Fit tests are beyond the scope of this article, my hope is that you'll get interested in learning more about this powerful framework. In addition to the websites listed previously, extensive information concerning the use of Fit and FitLibrary, an extension to Fit, may be found in Fit for Developing Software by Rick Mugridge and Ward Cunningham.

Running ASPX "Smoke Tests" with NUnitAsp

At this point, we've unit tested the domain layer and the data-access layer and learned about testing with a rough presentation layer, using Fit, for getting clients more involved. It's now time to test the ASPX pages themselves. NUnitAsp is a class library for performing these types of unit tests. Although you can get rather sophisticated using NUnitAsp with your WebForms testing, I find that NUnitAsp is best for running "smoke tests" from the continuous integration server to verify that no page is blatantly breaking. Taking NUnitAsp further than this tends to result in a lot of maintenance of the associated unit testing code. Since these HTTP unit tests are slow by nature, they're rarely run and, consequently, lightly maintained; therefore, they should be kept as simple as possible. BasicSample.Tests/Web/WebSmokeTests.cs demonstrates a sampling of these unit tests. Although trivially simple, these smoke tests go a long way towards verifying that your presentation layer is responsive, that database communications are working correctly, and that NHibernate HBMs are, for the most part, error-free. As an added bonus, if the smoke tests are directed at the production environment immediately after a deployment, they serve to pre-load all the ASPX pages for a more responsive experience for the very next visitor. You should include a smoke test for every URL-accessible web page in your application. To help organize them, create a separate test class for each grouping of smoke tests. For example, all the smoke tests for an admin section of the website would be found in a file called AdminSmokeTests.cs.

BasicSample.Core for Defining the Domain Layer

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

Separated Interface, Implemented

You'll notice that the BasicSample.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 BasicSample.Data. As described previously, this technique is called Separated Interface. If you consider BasicSample.Core to be an "upper-level layer" and BasicSample.Data to be a "lower-level layer", then, as Robert 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".

To see this in action, the data interfaces are described in the namespace BasicSample.Core.DataInterfaces. IDao 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 DAO test doubles for unit testing purposes. (This is an example of using the abstract factory pattern.) And as previously examined in BasicSample.Tests, leveraging mock objects in unit tests provides a means for testing a single responsibility at a time.

Collection Generics Examined

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". In the previous edition of this article, Ayende's very useful, but deprecated, NHibernate.Generics was used to integrate NHibernate with .NET generics. But now that NHibernate 1.2 natively supports generics, this class library is no longer necessary. If you've used Ayende's library in the past, you've got a bit of work ahead of you to completely migrate away from it, especially if you used the automatic wiring for managing parent/child relationships. But don't let this stop you as you can still upgrade to NHibernate 1.2 without having to immediately refactor out the automatic wiring...but you'll still want to do so sooner rather than later. More information about the steps required to refactor away from Ayende's NHibernate.Generics is found below in Migrating from NHibernate 1.0x to 1.2. A drawback to Ayende's NHibernate.Generics was that internal collections needed to be exposed with both getters and setters. This broke encapsulation and allowed collections to be manipulated or modified in unintended ways - think of it as collection harassment. Now that NHibernate supports generics natively, better collection encapsulation techniques may be employed. The following code from Customer.cs and Customer.hbm.xml shows a better encapsulation of generic collections.

public IList<Order> Orders {
get { return new List<Order>(orders).AsReadOnly(); }
protected set { orders = value; }
}
public void AddOrder(Order order) {
if (order != null && !orders.Contains(order)) {
orders.Add(order);
}
}
public void RemoveOrder(Order order) {
if (orders.Contains(order)) {
orders.Remove(order);
}
}
private IList<Order> orders = new List<Order>();

<bag name="orders" table="Orders" inverse="true" cascade="all">
<key column="CustomerID" />
<one-to-many class="BasicSample.Core.Domain.Order, BasicSample.Core" />
</bag>

Setting the orders collection setter to protected allows NHibernate to populate the collection without having to depend on a private member directly and without having to expose the setter publicly. Alternatively, the NHibernate setting access="field" could be used to set a private member directly, but the use of this should be carefully considered and only used when warranted. In the sample code above, note what AddOrder and RemoveOrder have done to the Customer class. They've shamelessly polluted the class by adding collection management concerns to the containing class. Imagine the headache this would turn into if Customer ended up having a number of collections along with methods for managing each one. In instances like this, it's usually best to employ a custom collection.

As a simple example of using a custom collection, the Supplier class may have a number of Product items associated with it. The caveat is that NHibernate needs to map to an IList collection; consequently, two collections are maintained. The first is the products collection itself, which NHibernate is aware of, and the second is a products collection wrapper which exposes the custom collection. The following code from Supplier.cs and Products.cs demonstrates this.

// Within Supplier.cs...
public Products Products {
get {
if (productsWrapper == null) {
productsWrapper = new Products(products);
}
return productsWrapper;
}
}
private Products productsWrapper;
// NHibernate binds directly to this member by the access="field" setting
private IList
 products = new List<Product>();
// Within Products.cs
public Products(IList
 products) {
this.products = products;
}
public void Add(Product product) {
if (product != null && !products.Contains(product)) {
products.Add(product);
}
}
private IList
 products;

Although simplistic, the example code should cover most of your custom collection needs. But in some cases, a full-blown generic, custom collection is required. One of the most common scenarios includes creating a custom collection which implements BindingList. In these types of situations, it's necessary to leverage NHibernate's IUserCollectionType. A good example of implementing this may be found here.

Generic IDs and Object Comparisons

In BasicSample.Core, each persistable domain object inherits from DomainObject. This class handles most of the work involved with comparing two domain objects for equality. (A detailed discussion of this object may be found in a devlicio.us blog post.) DomainObject is also a generic class which accepts a data type declaring the ID type of the domain object. This generic property provides the ability to have one domain object which uses a string as an ID, such as Customer, and another to have a long as an ID, such as Order. It should be duly noted that in the previous edition of this article, the ID property had both a public getter and setter. Although the getter is essential, the public setter opened Pandora's box for corrupting existing data. Assume you retrieved a customer from the database and accidentally set its ID to another customer's ID. When the customer gets saved back to the database, its data overwrites that of the other customer. For a more subtle example, assume the Customer class is being used as a pseudo DTO being returned from an edit screen. With a public ID setter, the developer sets the ID and the applicable properties and returns it to another object which takes care of transferring the DTO information to the "real" customer pulled from the database. Mind you, both the DTO and the "real" customer are being passed around as instances of Customer. So if, during maintenance, a developer doesn't realize that the Customer being returned from the view should be treated as a DTO and not as a "real" customer, and proceeds to invoke save on it directly, then it's quite probable that a lot of live customer data has been lost since it was overwritten with the sparse DTO data. The primary weakness behind this is the fact that ID had a public setter. Therefore, it is best that a domain object's ID only be set by NHibernate when loading an object from the database and is hidden from public setting. But there are situations wherein a domain object has an assigned ID. (Personally, I find no place for assigned IDs and avoid them whenever possible, but understand that an argument can be made for their use. You can find a further rant in BasicSample.Web/AddCustomer.aspx.) For situations when an assigned ID is required, an interface is included called IHasAssignedId. This interface defines a method for setting the ID. So although it provides a doorway for setting the ID, it requires a bit more thought concerning its use and provides a good spot for including ID-assignment business logic. The following snippet exemplifies this within the Customer class.

public class Customer : DomainObject<string>, IHasAssignedId<string>
{
public void SetAssignedIdTo(string assignedId) {
Check.Require(!string.IsNullOrEmpty(assignedId),
"assignedId may not be null or empty");
// As an alternative to Check.Require, 
the Validation Application Block could be used for the following
Check.Require(assignedId.Trim().Length == 5,
"assignedId must be exactly 5 characters");
ID = assignedId.Trim().ToUpper();
}
...
}

An obvious drawback to not exposing a public ID setter is that this property becomes essentially unusable to the unit testing framework, unless NHibernate is used to load the object. To skirt this problem, the class BasicSample.Tests/Domain/DomainObjectIdSetter.cs enables you to set the ID of domain objects, even if they do not implement IHasAssignedId. This ability opens many testing possibilities without having to provide a public setter to the ID property. On the flipside, using reflection to set private members, for the benefit of unit tests, should be seen as a non-standard practice and used only when absolutely necessary. It adds complexity and is fragile since it is string based. But for setting the ID property of domain objects, it's a perfect fit for the unit testing layer.

Mapping the Domain to the Database

NHibernate provides two means of mapping domain objects to the database: HBMs using XML and mapping attributes. The primary advantage of HBM-XML files is that they are physically separated from the domain objects they describe. This enables the domain objects to remain as POCOs (plain old C# objects), relatively oblivious to how they are associated with the database. But keeping the mapping information separated from the domain objects may also be seen as the primary disadvantage of HBMs in that it requires additional maintenance effort to keep switching between HBMs and the classes they map. (Some people also loathe using XML.) Mapping attributes, on the other hand, are intimately connected to the domain objects and are generally less verbose than their HBM equivalents. Using mapping attributes makes the domain objects more akin to Active Records than POCOs. (For true Active Record support, consider using Castle Project's ActiveRecord.) Besides "dirtying" up the domain objects, mapping attributes require a reference to NHibernate.Mapping.Attributes which makes the domain layer a bit less data-access-provider agnostic. On the other hand, do you often find yourself switching out data-access layers completely, anyway? But as a general rule, the domain layer should remain as data-access-provider agnostic as is practical for the design goals of the application. When it comes right down to it, it's a matter of personal preference when deciding between HBMs or mapping attributes. Regardless, when starting a new project, it should be decided which technique will be used; mixing the techniques may lead to confusion as it may not be clear which objects are mapped and which are not. The sample application demonstrates using HBMs for the mapping solution. The following snippet - not found in the sample application - shows an example of using mapping attributes instead of HBMs. Additional information concerning mapping attributes may be found in the NHibernate docs.

[NHibernate.Mapping.Attributes.Class]
public class Customer
{
[NHibernate.Mapping.Attributes.Property]
public string FirstName { ... }
...
}

NHibernate Support for Nullables

A new capability that NHibernate 1.2 brings to the table is support for nullable types. Previously, a reference to Nullables.NHibernate supported nullable types, but it is no longer needed. There's no need to treat nullable property mappings, within the HBMs, any differently than other property mappings; simply map the nullable property as you would any other. NHibernate is smart enough to transfer null between the database and the nullable property it maps to. Take a look at BasicSample.Core/Domain/Order.cs for an example of using a nullable DateTime. Although support for nullables is now trivially simple to implement, it should only be used after careful consideration. My own experiences have led me to hardly ever crea


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
ASP.NET PostedFile.ContentType所有类型对应值发布时间:2022-07-10
下一篇:
[Asp.net MVC]页面伪静态实现发布时间:2022-07-10
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap