August 19, 2008
@ 12:02 PM

I am currently attending the Devscovery conference on the Microsoft campus. So this is a perfect opportunity to meet up with my friends in the local ALT.NET group while I am here.

I turned on the batsignal and it was determined that Celtic Bayou was the location and this coming Thursday night at 5:30 is the time.

So if you are in town and have some free time, come join us and discuss all things software with fellow passionate developers. The beer is great and the food is even better. See you there.

 

What: Geek Dinner

When: Thursday, Aug 21st 2008 5:30 PM - Till we all leave

Where: Celtic Bayou


 
Categories: Events | Local

I will be attending the Wintellect Devscovery conference this week, August 19th - 21st, on the Microsoft campus in Redmond. If any ALT.NET people want to get together for lunch or dinner while I am there feel free to contact me via the plethora of options to the right. I will be in full on geek out mode and happy to talk with you.

Here is my schedule while I am there:

 

Tuesday  
9:00 AM Keynote - I am ASP.NET 3.5 Extensions and So Can You! - Hanselman
10:45 AM Translating Architecture to Technologies - Dahlman
12:15 PM Lunch
1:30 PM An Overview of ASP.NET MVC - Haack
3:15 PM An Introduction to TDD - Haack
Wednesday  
9:00 AM Windows Communication Foundation: Rest with WCF - Mehner
10:45 AM Windows Communication Foundation: Debugging & Error Handling - Mehner
12:15 PM Lunch
1:30 PM Practical Workflow Foundation, Part 1 - Mehner
3:15 PM Practical Workflow Foundation, Part 2 - Mehner
Thursday  
9:00 AM An Introduction to LINQ to SQL - Demsak
10:45 AM An Introduction to LINQ to Entities - Demsak
12:15 PM Lunch
1:30 PM Unit Testing & Code Coverage Best Practices - Robbins
3:15 PM .NET Performance Tips & Tricks - Robbins

 
Categories: Development | Events | Local

 Taken in Blijdorp Michael Feathers, the amazing author of Working Effectively with Legacy Code, posted recently about technical debt.

What happens to code when you don’t refactor?  Anyone with any experience knows the answer.  It gets messy.  It becomes hard to change and the rate at which you can add features slows to a crawl.

I was on a project that inherited a tremendous amount of technical debt. The motivating force for the original team was meeting a hard deadline. As the deadline approached development process crumbled. Hero developers shot from the hip. Corners were cut. Non-critical bugs piled up. Quality was sacrificed to meet the deadline.

The technical debt was put on the American Express.

At the end of the project, the resulting application had a week long eventful launch full of rapid critical bug fix releases. The application was eventually brought up to an acceptable level of performance through Herculean effort of very skilled and talented people.

The technical debt bill was left ignored.

The following project cycle a new team was introduced to the application. A feature set to be added was defined. A deadline was set. A new requirement was added. All new features would have a emphasis on quality. No defects would be added to the already limping system.

The technical debt went into Universal Default and the APR was bumped up to 29%.

The team began moving forward except this time the business actively using the system kept running into critical issues that the team had to quickly deal with. The short cuts taken by the previous team became direct roadblocks to the new features slowing the implementation down. Each new line of code had to make sacrifices to work around quirks in the code base.

The 800 pound technical debt collection gorilla started calling demanding payment.

After a year of use the sheer size of the technical debt in the database caused leaks to spring which cascaded outward through out the application. New releases were viewed with dread by the entire organization. The DBA's were not happy. The help desk was not happy. The users were not happy. Morale on the project was through the floor.

The 800 pound technical debt collection gorilla had frozen all assets and put a lean on the house.

You can put that debt on the card. You can ignore it and buy some time. But eventually that gorilla is going to find you, climb on your back and beat you down.

Photo Credit:  Ruben Bos


 
Categories: Commentary | Development | Random

Introduction

As you may have noticed, I have become interested in the concept of Object Relational Mapping and the NHibernate framework. One of the more painful/tedious aspects of using NHibernate is hand writing the xml mapping files. That is why I got excited when I heard that Jeremy Miller was open sourcing his mapping generation libraries.

FHLogo The Fluent NHibernate project is an effort to create a set of APIs that generate NHibernate mapping files using a fluent interface. I downloaded the source code from the Google Code Repository and quickly found myself adding fluent methods. I submitted my changes to the project and was accepted as a contributor.

One of the tasks identified by the project owner, James Gregory, was to create a quick start guide that easily fit into the NHibernate Quick Start section 1.3 Mapping the cat. I assigned the task to myself and start hacking some code.

Creating the Mapping Class

First, create a new class project QuickStart.Domain to hold  domain model objects that need to be mapped. To this assembly,  add the Cat class from the NHibernate quick start.

namespace QuickStart.Domain
{
    public class Cat
    {
        public virtual string Id { get; set; }

        public virtual string Name { get; set; }

        public virtual char Sex { get; set; }

        public virtual float Weight { get; set; }
    }
}

This is what the class looks like after a little ReSharper Code Clean Up loving.

Then add a second class project QuickStart.Domain.Mapping to hold  domain model mapping classes using the Fluent NHibernate library. Add references to both the FluentNHibernate.dll and the domain model library.

Create a new class in the mapping library called CatMap. This class will inherit from ClassMap<T> where T is the type you are creating the map for, in this case Cat. Create a constructor for the CatMap class. The constructor is where the mappings will be defined.

Because CatMap is an instance of ClassMap, you can begin using the fluent interface in the constructor right away.

public CatMap()
        {
            this.TableName = "Cat";
        }

The TableName property of the ClassMap object specifies the name of the table in the data store that stores the Cat class. This explicit setting of the table name is unnecessary. If one is not provided the API will assume the table has the same name as the class being mapped. So, for the rest of this example, it will be dropped.

The API offers several fluent methods for defining an identifier though the Id method of ClassMap. The Cat example uses the UUID generator which looks like this:

 

public CatMap()
        {           
            this.Id(x => x.Id)
                .GeneratedBy
                .UuidHex("B");
        }
 

An identity column in SQL server would be mapped like this:

public CatMap()
        {
            Id(x => x.Id);
        }

These two examples take advantage of the new C# 3.0 syntax sugar lambda expressions. An explanation of lambda expressions is outside the scope of this article, but  tons of information is available on the topic on the web.

The remainder of the CatMap constructor uses the Map method of ClassMap to define the remaining properties of the class.

public CatMap()
        {
            this.TableName = "Cat";
           
            this.Id(x => x.Id)
                .GeneratedBy
                .UuidHex("B");

            //non-nullable string with a length of 16
           this.Map(x => x.Name)
                .WithLengthOf(16)
                .CanNotBeNull();

            //simple properties
            this.Map(x => x.Sex);
            this.Map(x => x.Weight);
        }

Both the Sex and Weight properties of the Cat class are mapped quickly with a single call to them Map method. You do not need to explicitly specify the type of your properties, Fluent NHibernate will infer it based on the type being mapped. The Name property has an additional two fluent calls to limit the length of the property to sixteen characters and to disallow null values.

Usage of this mapping generates the following XML document:

<?xml version="1.0" encoding="utf-8"?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" 
  default-lazy="false" 
  assembly="QuickStart.Domain" namespace="QuickStart.Domain">
  <class name="Cat" table="Cat" xmlns="urn:nhibernate-mapping-2.2">
    <id name="Id" column="Id" type="String" unsaved-value="0">
      <generator class="uuid.hex">
        <param name="format">B</param>
      </generator>
    </id>
    <property name="Weight" column="Weight" type="Single">
      <column name="Weight" />
    </property>
    <property name="Sex" column="Sex" type="Char">
      <column name="Sex" />
    </property>
    <property name="Name" column="Name" length="16" type="String" not-null="true">
      <column name="Name" />
    </property>
  </class>
</hibernate-mapping>

This mapping xml is a bit more verbose than the original example from the NHibernate quick start, but this post is being written using an alpha version of the Fluent NHibernate library. There is a lot of clean up and work left to be done.

Hooking It All Up

So, how exactly did I go from CatMap to generated XML document? There is currently no recommended methodology that I am aware of, but I am happy to share how I accomplished it.

I started by adding an interface to the Mapping library called IMapGenerator that looks like this:

namespace QuickStart.Domain.Mapping
{
    public interface IMapGenerator
    {
        string FileName { get; }
        XmlDocument Generate();
    }
}

 

FileName is defined in the fluent interface on the ClassMap class, I know I added it while writing this code ;). It represents the conventional name for the NHibernate mapping files. For example, We are mapping the class Cat so FirstName would contain the string "Cat.hbm.xml". Generate on the other hand, will be a wrapper around the ClassMap's CreateMapping method.

I then added the interface to the CatMap class. The final CatMap looks like this:

namespace QuickStart.Domain.Mapping
{
    public class CatMap : ClassMap<Cat>, IMapGenerator
    {
        public CatMap()
        {
             Id(x => x.Id)
                .GeneratedBy
                .UuidHex("B");

            Id(x => x.Id);

            //non-nullable string with a length of 16
            Map(x => x.Name)
                .WithLengthOf(16)
                .CanNotBeNull();

            //simple properties
            Map(x => x.Sex);
            Map(x => x.Weight);
        }
        
        public XmlDocument Generate()
        {
            return CreateMapping(new MappingVisitor());
        }
    }
}

 

I now have a way to identify all my mapping classes using the interface. I wanted to be able to automatically get a list of all the classes that implement the IMapGenerator interface, so  created a helper class GeneratorHelper with a single static method GetMapGenerators. The class lives in the Mapping library and looks like this:

namespace QuickStart.Domain.Mapping
{
    public class GeneratorHelper
    {
        private const string GENERATOR_INTERFACE = "IMapGenerator";

        public static IList<IMapGenerator> GetMapGenerators()
        {
            IList<IMapGenerator> generators = new List<IMapGenerator>();
            Assembly assembly = Assembly.GetAssembly(typeof(IMapGenerator));
            foreach (Type type in assembly.GetTypes())
            {
                if (null == type.GetInterface(GENERATOR_INTERFACE)) continue;
                var instance = Activator.CreateInstance(type) as IMapGenerator;
                if (instance != null)
                    generators.Add(instance);
            }
            return generators;
        }
    }
}

This method uses reflection to locate and load the assembly that contains the IMapGenerator interface. It then iterates over the types in the loaded assembly and checks to see if the type implements the IMapGenerator interface. If a match is found, an instance of that class is created and added to the generators list.

Finally, I created a console application, QuickStart.Domain.Mapping.Mapper, and added a reference to my Mapping library. The implementation of my console app is fairly straight forward.

 

namespace QuickStart.Domain.Mapping.Mapper
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            IList<IMapGenerator> generators =
                GeneratorHelper.GetMapGenerators();
            
            foreach (IMapGenerator generator in generators)
            {
                XmlDocument classMapXML = generator.Generate();
                classMapXML.Save(generator.FileName);
            }
        }
    }
}

 

The app calls the generator helper class to get a list of IMapGenerator objects which it then iterates over calling each objects Generate method and saves the result using the conventional name for mapping files. It works fairly well and I can add as many mapping classes as I need to the Mapping library and spin out xml any time I need.

Now where Fluent NHibernate becomes really interesting is when you decide to do away with xml mapping files all together. For a great example of this check out Zachariah Young's post, "Does the Fluent NHibernate create static XML mapping files?".


 
Categories: Development | Fluent Interface | NHibernate | Tools

At the end of yesterdays internal study group meeting, the organizer mentioned that I would be giving a presentation on NHibernate the following week. I plan to give a simple dog and pony show of persisting a simple object to the database and all of the setup needed to accomplish the task.

One of the participants asked for a brief description of NHibernate. I explained that it is an ORM framework (Object Relational Mapping). I expanded a bit by saying that NHibernate removes the dependency between your domain objects and your relational database by handling mapping from one to the other.

After a couple more questions it was determined that NHibernate generates SQL and is therefore inferior to stored procedures. It was the general feeling that stored procedures were the be all end all for performance and optimization. I attempted to explain that NHibernate generates parameterized queries which have all the advantages of cached query plans that stored procedures do. By this time the meeting was breaking up and I had a mission.

I wanted to challenge the dogmatic urban legend passed down from .NET developer to .NET developer since the classic ASP days. That dogma simply states you should always access your database via stored procedure for "performance reasons". Inline SQL is BAAAAAAD.

So I fired up Visual Studio, created a class and started writing tests.

The first thing I wanted to do was to create a baseline. I created a User table in a fresh SQLExpress database and wrote a test to pop 1000 rows into it in the crustiest "I just read my first Wrox book" way I could think of. I added some timing code and let it rip. The test looks like this:

 

       [Test]
        public void Time_Inline_Inserts_To_User_Table()
        {
            DateTime start = DateTime.Now;

            using (SqlConnection c = new SqlConnection(connectionString))
            {
                c.Open();
                for (int i = 0; i < INSERT_COUNT; i++)
                {
                    using (SqlCommand co = new SqlCommand())
                    {
                        co.Connection = c;
                        co.CommandText =
                            "INSERT INTO dbo.Users "
                            + "(Handle, FirstName, LastName, Password, EmailAddress, LastLogon) "
                            + "VALUES ("
                            + "'UserHandle" + i + "',"
                            + "'UserFirstName" + i + "',"
                            + "'UserLastName" + i + "',"
                            + "'UserPassword" + i + "',"
                            + "'User" + i + "@email.com',"
                            + DateTime.Now.ToShortDateString() + ")";

                        co.ExecuteNonQuery();
                    }
                }
                c.Close();
            }
            DateTime end = DateTime.Now;
            TimeSpan time = end - start;

            Console.WriteLine(
                string.Format("{0} milliseconds to run {1} inserts by inline query", 
                time.TotalMilliseconds, INSERT_COUNT));
        }

From a fresh load of my test assembly into MbUnit, this code takes 1312.5 milliseconds to run 1000 inserts. Running the test repeatedly drops the time to 437.5 miliseconds. It appears that SQL Server does some optimizations for inline inserts regardless of what the tribe tells me.

Next up, I wanted to see if parameterizing the query would improve the performance. The test case looks like this:

        [Test]
        public void Time_Parameterized_Query_Inserts_To_User_Table()
        {
            DateTime start = DateTime.Now;

            using (SqlConnection c = new SqlConnection(connectionString))
            {
                c.Open();
                for (int i = 0; i < INSERT_COUNT; i++)
                {
                    using (SqlCommand co = new SqlCommand())
                    {
                        co.Connection = c;
                        co.CommandText =
                            "INSERT INTO dbo.Users (Handle, FirstName, LastName, Password, EmailAddress, LastLogon) VALUES (@p1,@p2,@p3,@p4,@p5,@p6)";
                        co.CommandType = System.Data.CommandType.Text;
                        co.Parameters.AddWithValue("@p1", "UserHandle" + i);
                        co.Parameters.AddWithValue("@p2", "UserFirstName" + i);
                        co.Parameters.AddWithValue("@p3", "UserLastName" + i);
                        co.Parameters.AddWithValue("@p4", "UserPassword" + i);
                        co.Parameters.AddWithValue("@p5", "User" + i + "@email.com");
                        SqlParameter param = new SqlParameter("@p6", SqlDbType.DateTime);
                        param.Value = new DateTime(2000, 1, 1);
                        co.Parameters.Add(param);

                        co.ExecuteNonQuery();

                    }
                }
                c.Close();
            }

            DateTime end = DateTime.Now;
            TimeSpan time = end - start;

            Console.WriteLine(
                string.Format("{0} milliseconds to run {1} inserts by parameterized query", 
                    time.TotalMilliseconds, INSERT_COUNT));

        }

This test runs in 1296.9 milliseconds on a fresh load of my test assembly. Repeated runs drops the test run to ~350 milliseconds a slight advantage over the inline query. I imagine this gap would widen with a more complex query.

Finally, we have the acclaimed SQL Stored Procedure dogmatic method of how everything should be done! The stored procedure in the code below is pretty much exactly what you expect it to be.

       [Test]
        public void Time_Stored_Proceedure_Inserts_To_User_Table()
        {
            DateTime start = DateTime.Now;

            using (SqlConnection c = new SqlConnection(connectionString))
            {
                c.Open();
                for (int i = 0; i < INSERT_COUNT; i++)
                {
                    using (SqlCommand co = new SqlCommand())
                    {
                        co.Connection = c;
                        co.CommandText = "AddUser";
                        co.CommandType = CommandType.StoredProcedure;
                        co.Parameters.AddWithValue("@p1", "UserHandle" + i);
                        co.Parameters.AddWithValue("@p2", "UserFirstName" + i);
                        co.Parameters.AddWithValue("@p3", "UserLastName" + i);
                        co.Parameters.AddWithValue("@p4", "UserPassword" + i);
                        co.Parameters.AddWithValue("@p5", "User" + i + "@email.com");
                        SqlParameter param = new SqlParameter("@p6", SqlDbType.DateTime);
                        param.Value = new DateTime(2000, 1, 1);
                        co.Parameters.Add(param);

                        co.ExecuteNonQuery();

                    }
                }
                c.Close();
            }

            DateTime end = DateTime.Now;
            TimeSpan time = end - start;

            Console.WriteLine(
                string.Format("{0} milliseconds to run {1} inserts by stored proceedure", 
                    time.TotalMilliseconds, INSERT_COUNT));

        }

This test runs unexpectedly in 1390.6ms. This seems like quite a long time for a stored procedure that is stored in the database and from my understanding with a query plan in place. Subsequent executions of the test yield execution times ~320ms. Just to verify this, I closed the MbUnit test runner application and reran these tests. I got the same results.

Now that we have a baseline of the various direct SQL methods that are well accepted in the .NET community, I want to see how NHibernate performs at these tasks. I see two methods available though NHibernate spinning up a session and cramming User objects into its Save() method and adding a transaction into the mix.

My first NHibernate test looks like this:

       [Test]
        public void Time_NHibernate_Inserts_To_User_Table()
        {
            DateTime start = DateTime.Now;
            using (ISession session = factory.OpenSession())
            {
                for (int i = 0; i < INSERT_COUNT; i++)
                {
                    User u = new User();
                    u.Handle = "UserHandle" + i;
                    u.FirstName = "UserFirstName" + i;
                    u.LastName = "UserLastName" + i;
                    u.Password = "UserPassword" + i;
                    u.EmailAddress = "User" + i + "@email.com";
                    u.LastLogon = DateTime.Now;

                    session.Save(u);

                }
                session.Close();

            }

            DateTime end = DateTime.Now;
            TimeSpan time = end - start;

            Console.WriteLine(
                string.Format("{0} milliseconds to run {1} inserts by nhibernate", 
                    time.TotalMilliseconds, INSERT_COUNT));

        }

This test runs in 1656.3ms with reruns clocking in at ~560ms. It looks like NHibernate might have a slight performance hit when compared to stored procedures.

My last test using NHibernate transactions looks like this:

       [Test]
        public void Time_NHibernate_Tansaction_Inserts_To_User_Table()
        {
            DateTime start = DateTime.Now;
            using (ISession session = factory.OpenSession())
            {

                ITransaction transaction = session.BeginTransaction();
                for (int i = 0; i < INSERT_COUNT; i++)
                {
                    User u = new User();
                    u.Handle = "UserHandle" + i;
                    u.FirstName = "UserFirstName" + i;
                    u.LastName = "UserLastName" + i;
                    u.Password = "UserPassword" + i;
                    u.EmailAddress = "User" + i + "@email.com";
                    u.LastLogon = DateTime.Now;

                    session.Save(u);

                }
                transaction.Commit();
                session.Close();

            }

            DateTime end = DateTime.Now;
            TimeSpan time = end - start;

            Console.WriteLine(
                string.Format("{0} milliseconds to run {1} inserts by nhibernate with transaction", 
                    time.TotalMilliseconds, INSERT_COUNT));

        }

This test runs in 1406.3ms with follow up runs in 300ms. A dramatic increase in performance over over the non transactional processing. So the final performance test results looks something like this:

Test 1st Run nth Run
Inline SQL 1312.5ms ~430ms
Parameterized Query 1296.9ms ~350ms
Stored Procedure 1390.6ms ~320ms
NHibernate 1565.3ms ~560ms
NHibernate w/Transaction 1406.3ms ~300ms

I am not going to try to interpret these results, but simply publish them and see what feedback I get from the collective. My next steps are to increase the complexity of what I am doing; add some joins into the mix. I want to see if the performance gap widens or a clear winner emerges.

If you would like to see the entire code base for these tests, I have published them to my CodePlex repository and would love to hear your ideas on squeezing out performance with both SQL and NHibernate. Look for the NHibTester solution.


 
Categories: Performace | Tools | Unit Testing

In a recent post, I discovered a flaw in my world domination plot. I had written a Unit Test to test my expectation that the method GetByID() of the class UserRepository returned the expected User. When UserRepository.GetById() is called, it calls IDataProvider.GetById(), which returns an instance User to the UserRepository which in turn returns it to the caller.

My UserRepository class delegates database access to the interface IDataProvider. This allows me to substitute any implementation of IDataProvider to satisfy this responsibility. In non testing code I use Dependancy Injection to map UserDataProvier to IDataProvider. But in testing code I do not really want to hit a database as it slows the testing process down. So I use Rhino.Mocks instead.

Rhino.Mocks is a framework for creating mock objects for use in Unit Testing. Provide Rhino.Mocks with an Interface and it will give you back an object that implements that interface. You can also tell it to expect certain calls against the interface and how it should respond.

My Unit Test looked something like this:

      [Test]
        public void GetByIdTest()
        {
            MockRepository mock = new MockRepository();

            IDataProvider dataProvider = (IDataProvider)mock.CreateMock<IDataProvider>();
            UserRepository target = new UserRepository(dataProvider);

            Expect.Call(dataProvider.GetById(1)).Return(new User() { Id = 1 });

            mock.ReplayAll();
            User u = target.GetById(1);
            mock.VerifyAll();

            Assert.AreEqual(1, u.Id);
        }

As you can see this test, creates a MockRepository that is provided by the Rhino.Mocks framework. It then declares an instance of IDataProvider but delegates the creation of that instance to the MockRepository's CreateMock method. This tells Rhino.Mocks to create a dummy instance of the IDataProvider interface. Next we new up a UserRepository (the actual target of the unit test) and provide our mocked IDataProvider to it's constructor. Then we tell Rhino.Mocks to expect a call to our mock IDataProvider's GetById method and to return a new instance of User with it's Id property set to 1. Finally, we do our testing and validation.

This test fails to compile. The entity class User has read only properties. Id happens to be one of them. In the domain model for this particular application Id is a unique identifier and once a User instance is returned from the data access layer, it should never be modified.

So this presents a unique challenge, how do I test that UserRepository.GetById(1) returns an instance of User with an Id of 1?

I went down the path of trying to use Ninject (an Inversion of Control container), to inject the value in a newed up instance. But this had code smell for me. Why am I creating a dependency on Ninject to get my Unit Tests to work. That just seemed wrong to me. So I began digging in the Rhino.Mocks documentation wiki to see if it had a method for resolving this.

That was when Aiden Montgomery in the #ALT.NET IRC channel suggested that I use Reflection. He even went so far as to download my source from CodePlex and demonstrate what he was suggesting in my application.

The final test ended up looking something like this:

[Test]
        public void GetByIdTest()
        {
            MockRepository mock = new MockRepository();
            
            Type userType = typeof(User);
            PropertyInfo pi = userType.GetProperty("Id");
            User user = new User();
            pi.SetValue(user, 1, null);

            IDataProvider dataProvider = 
                (IDataProvider) mock.CreateMock<IDataProvider>();
            UserRepository target = new UserRepository(dataProvider);

            Expect.Call(dataProvider.GetById(1)).Return(user);

            mock.ReplayAll();
            User u = target.GetById(1);
            mock.VerifyAll();

            Assert.AreEqual(1, u.Id);
        }

 

This of course passed, didn't add a non-BCL dependency and maintained the original intent of the test and the domain model. With all the shiny new toys, I was forgetting to see the forest through the trees and return to the simplest solution. Thanks to Aiden for bringing me back down from the clouds.


 
Categories: Development | Tools | Unit Testing

Karl Seguin of CodeBetter.com released a 79 page ebook called Foundations of Programming yesterday. Browsing the table of contents, I found a ton of good information is covered here. Some highlights include: YAGNI & DRY principals, Explicitness, Cohesion & Coupling, Domain Driven Design... The list just goes on and on and on.

If you want to read about the stuff that a giant Wrox tomb will never mention, grab this ebook as a great launching point.


 
Categories: Development | Events | Fundamentals

Stephen Bohlen of Microdesk recently posted to the ALT.NET mailing list that he plans to release his internal training material on NHibernate to the world at large. The first two, of a planned  5, are online and ready for consumption.


I have been doing some noodling with NHibernate recently, so I downloaded the screen casts and watched them this morning. The quality is excellent, the material is comprehensive and Stephen's teaching style is top notch.

If you are interested in learning what NHibernate is all about and what you can do with it, I cannot recommend these enough. Grab them here:


 
Categories: Development | Fundamentals | Tools