A good deed

Looks like I did a good deed today :)

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

Repository implementation

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

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

public static class NHibernateHelper

{

    private static ISessionFactory _sessionFactory;

 

    private static ISessionFactory SessionFactory

    {

        get

        {

            if (_sessionFactory == null)

            {

                var configuration = new Configuration();

                configuration.Configure();

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

                _sessionFactory = configuration.BuildSessionFactory();

            }

            return _sessionFactory;

        }

    }

 

    public static ISession OpenSession()

    {

        return SessionFactory.OpenSession();

    }

}

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

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

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

  <session-factory>

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

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

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

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

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

    <mapping assembly="Peniko.DataLayer" />

  </session-factory>

</hibernate-configuration>

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

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

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

{

    void Save(T entity);

    void Delete(T entity);

    T GetById(Guid id);

    IList<T> GetAll();

}

 

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

{

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

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

 

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

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

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

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

}

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

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

This is a Save method from Repository<T>:

public void Save(T entity)

{

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

 

    using (var session = NHibernateHelper.OpenSession())

    using (var transaction = session.BeginTransaction())

    {

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

 

        try

        {

            session.SaveOrUpdate(entity);

            AfterSaveEntity(session, entity);

            transaction.Commit();

        }

        catch (StaleObjectStateException ex)

        {

            transaction.Rollback();

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

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

        }

        catch (HibernateException ex)

        {

            transaction.Rollback();

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

            throw;

        }

    }

}

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

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

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

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

GetById looks like this:

public T GetById(Guid id)

{

    using (var session = NHibernateHelper.OpenSession())

    {

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

 

        try

        {

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

            AfterGetEntity(session, entity);

            return entity;

        }

        catch (HibernateException ex)

        {

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

            throw;

        }

    }

}

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

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

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

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

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

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

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

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

Below is an example of a very simple mapping:

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

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

                  assembly="Peniko.Domain"

                  namespace="BLDotNet.Peniko.Domain">

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

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

      <generator class="guid" />

    </id>

 

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

  </class>

</hibernate-mapping>

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

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

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

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

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

Unit Testing the Domain

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

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

namespace BLDotNet.Peniko.Domain.Tests

{

    [TestFixture]

    public class CategoryFixture

    {

        private const int NameMaxLength = 50;

 

        private Category _category;

 

        [SetUp]

        public void SetUp()

        {

            _category = TestGlobals.GetValidCategory();

        }

 

        [Test]

        public void CanCreateCategory()

        {

            Assert.IsTrue(_category.IsValid);

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

        }

 

        [Test]

        public void CannotHaveNullName()

        {

            _category.Name = null;

            Assert.IsFalse(_category.IsValid);

        }

 

        [Test]

        public void CannotHaveEmptyName()

        {

            _category.Name = String.Empty;

            Assert.IsFalse(_category.IsValid);

        }

 

        [Test]

        public void CanHaveMaxLengthName()

        {

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

            Assert.IsTrue(_category.IsValid);

        }

 

        [Test]

        public void CannotHaveTooBigName()

        {

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

            Assert.IsFalse(_category.IsValid);

        }

    }

}

And here is how the Category class looks like:

namespace BLDotNet.Peniko.Domain

{

    public class Category : Entity<Category>

    {

        [NotNullValidator]

        [StringLengthValidator(1, 50,

            MessageTemplateResourceName = "CategoryNameLength",

            MessageTemplateResourceType = typeof (Category))]

        public virtual string Name { get; set; }

    }

}

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

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

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

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

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

You are an idiot!

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

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

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

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

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

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

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

Are you valid?

The next thing we wanted to implement to our Core project was a business object validation. There are several paths we could take:

Because we qualified this application as a learning project, and we haven't used VAB before, we decided to try it. VAB enables developers to attach validation rules to properties in declarative way, by using attributes. Rules can also be defined in config file, allowing the simple rules change after the application is deployed, but that wasn't a requirement for this project.

Most of our entity classes now have properties that look like this:

[NotNullValidator]

[StringLengthValidator(3, 15,

    MessageTemplateResourceName = "ProductCodeLength",

    MessageTemplateResourceType = typeof (Product))]

public virtual string Code { get; set; }

 

[NotNullValidator]

[StringLengthValidator(1, 50,

    MessageTemplateResourceName = "ProductNameLength",

    MessageTemplateResourceType = typeof (Product))]

public virtual string Name { get; set; }

In order to avoid cluttering of code with localized error messages, they are moved to project resources, hence the MessageTemplateResourceName and MessageTemplateResourceType arguments.

And now the big thing... In order to check the object validity, IsValid() method is added to Entity<T> class (base class for all our entity objects, defined as Entity<T> where T: Entity<T>). IsValid is calling VAB Validator to check the property values on the object and returns true if object is valid. The first version looked like this:

public virtual bool IsValid()

{

    return Validation.Validate((T)this);

}

Validation class is a façade. It creates a Validator object and tests for validity in a single line of code. Validate method is actually generic - Validation.Validate<T>((T)this), but T argument is redundant.

The cast to type T was necessary, because the Validator isn't smart enough to go down the inheritance hierarchy. If we call Validate(this), it would try to find validation rules on the current class (Entity<T>), but not on inheriting classes. I.e. public class Product : Entity<Product>. By having a cast to T (Product in this case), we ensure that the rules are checked on the Product class.

This worked fine up until recently, when our entity model became more advanced:

public class Warehouse : Entity<Warehouse>

...

public class CommissionerWarehouse : Warehouse

...

IsValid method couldn't handle the CommissionerWarehouse class. The cast to T in Validate method gave it only a Warehouse type to check. CommissionerWarehouse rules were ignored. In order to handle this case, IsValid was rewritten to use the Validator object directly:

public virtual bool IsValid()

{

    var entityValidator = ValidationFactory.CreateValidator(GetType());

    return entityValidator.Validate(this).IsValid;

}

The code is twice as long as before, but I can live with it :)

The magic of this method allows easier Unit Testing (more on that later) and validation calls from the client without the reference to Validation Application Block assemblies. We could add some method to return error messages too, but that's not necessary, since we are using Martin Bennedik's WPF Integration for VAB.

The core of all applications

Today, I'll talk a little about the core of our application. There are many terms to describe it: business layer/model, entity layer/model, domain layer/model, middle layer... Whatever you want to call it, it still represents the problem you are trying to solve.

Being highly influenced by some articles we read, we mostly use Domain-Driven Design terms. So, our core project is called Peniko.Domain (Peniko is the name of my friends' company). This project started as a collection of POCO classes without much behavior, just enough to accommodate NHibernate's needs, but as we moving toward the higher layers of the application, it's getting more "intelligent". Below is the current class diagram.

Peniko.Domain class diagram

All class names are in English, except for PdvRate class. It should be VatRate (PDV = VAT), but we prefer our local acronym. You'll notice that all classes inherit from Entity<T> and below is how this class looked when it's created. The class is copied from here and renamed to Entity.

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

{

    private int? _oldHashCode;

 

    /// <summary>

    /// Gets or sets the object Id.

    /// </summary>

    public virtual Guid Id { get; protected set; }

 

    /// <summary>

    /// ...

    /// </summary>

    public override bool Equals(object obj)

    {

        var other = obj as T;

        if (other == null) return false;

 

        // Handle the case of comparing two NEW objects

        var otherIsTransient = Equals(other.Id, Guid.Empty);

        var thisIsTransient = Equals(Id, Guid.Empty);

        if (otherIsTransient && thisIsTransient)

            return ReferenceEquals(other, this);

 

        return other.Id.Equals(Id);

    }

 

    /// <summary>

    /// ...

    /// </summary>

    public override int GetHashCode()

    {

        // Once we have a hash code we'll never change it

        if (_oldHashCode.HasValue) return _oldHashCode.Value;

 

        var thisIsTransient = Equals(Id, Guid.Empty);

 

        // When this instance is transient, we use the base GetHashCode()

        // and remember it, so an instance can NEVER change its hash code.

        if (thisIsTransient)

        {

            _oldHashCode = base.GetHashCode();

            return _oldHashCode.Value;

        }

 

        return Id.GetHashCode();

    }

 

    public static bool operator ==(Entity<T> x, Entity<T> y)

    {

        return Equals(x, y);

    }

 

    public static bool operator !=(Entity<T> x, Entity<T> y)

    {

        return !(x == y);

    }

}

 

There is one more important concept added to Domain project. Repository pattern interfaces. We are reevaluating the decision to have the repository interfaces in domain layer, but for now they are there. The purpose of repositories is to communicate with the mapping layer (load entities, save and delete them). Because CRUD operations are common for all our repositories, we created a base IRepository<T> interface. It looks like this:

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

{

    void Add(T entity);

    void Update(T entity);

    void Delete(T entity);

    T GetById(Guid id);

    IList<T> GetAll();

}

Other interfaces inherit IRepository<T>, and add more methods if necessary. I.e. IProductRepository looks like this:

public interface IProductRepository : IRepository<Product>

{

    Product GetProductByCode(string code);

    Product GetProductByBarCode(string barCode);

}

The classes implementing Repository interfaces are located in Peniko.DataAccess project. I'll talk more about them more in one of the following posts.

NHibernate learning material

We started off by learning NHibernate. Up until then I didn't use any OR/M (I don't consider LINQ 2 SQL to be a full OR/M). Most of the time, the code I wrote used ADO.NET DataReaders, alone or inside CSLA.NET DataPortal_xxx methods.

Since there is a lot of buzz lately about OR/Ms, Entity Framework is about to be released, it already got a vote of no confidence from ALT.NET group, etc, the good learning material isn't so hard to find. When I'm learning something new, I'm trying to find and use best good practices, established by the people who have been used that technology for a longer time. The following posts/articles/screencasts really helped me and my team to get on the right track.

The NHibernate FAQ by Gabriel Schenker is a great starting point:

Steve Bohlen's excellent screencast series Summer of NHibernate.

Ben Scheirman's series of articles about NHibernate and Domain Driven Design:

Davy Brion's NHibernate posts.

THE article NHibernate Best Practices with ASP.NET by Billy McCafferty. I'm looking forward for a chance to try his S#arp Architecture on a real project.

David Veeneman's NHibernate Made Simple article.

All of the above links will not only teach you how to use NHibernate, but also give you a start with Domain Driven Design, Unit Testing, Integration Testing, logging etc. Enjoy!

I want to be like uncle Karl

In my previous post, I mentioned that I'm learning a lot of new stuff lately. So, what is it?

Actually, I'm trying to learn tools from ALT.NET toolbox. While I was always leaning towards software architecture, following Framework Design Guidelines and "best practices", reading and learning about new things in .NET community, somehow I've managed to avoid using things like Unit Testing, O/RM (I'm CSLA.NET guy :) and other I consider to be invaluable to software developer. Reading blogs like Ayende's, Roy's or Jeremy's, just to mention a few, always made me feel inferior, and that was a good thing. The first step to knowledge is acknowledging that "I know that I know nothing" :)

The series of posts that really inspired me was Karl Seguin's Foundation of Programming, also available as an ebook. I even made a New Year (2008) resolution to "be like uncle Karl" :)

Few months have passed without pursuing my goals, because of work / school obligations, and finally a month ago, I was asked to do a business application for my friend Branko's family business. It was a great, no time pressure, opportunity to learn, so I accepted.

Together with my friends Boris and Branko (yeah, the same Branko from above :) I started the journey. We have prepared application requirements, started learning NHibernate, Unit Testing (NUnit), validation (VAB), improving our WPF and of course implementing all that to the application.

The application is going along nicely. I'll try to post more about it in the following posts.

VisualSVN Server / Trac tips

Here are few tips I return to when installing or creating a new Trac environment:

VisualSVN Server Trac installation

VisualSVN Server failed to start once I've installed the Trac integration. Apache's log had an entry saying it cannot find mod_python.so file, although it was on the right place. After a bit of googling, I've managed to solve the problem by adding msvcr71.dll and msvcp71.dll to my Windows System folder. I can't give a credit to a person who found the solution, because I don't remember the page where I found it.

Creating Trac environment

Before you start, make sure there you have SVN repository prepared. Also, you should have at least one user that will get TRAC_ADMIN rights. Use VisualSVN Server Manager to create SVN repository and user(s).

If you have used defaults from Trac integration page, your SVN repositories will be in C:\Repositories\ and Trac environments in C:\Trac\.

  • Run a console and go to: C:\Program Files\VisualSVN Server\bin\python\Scripts
  • Run: trac-admin C:\Trac\<ProjectName> initenv
    • trac-admin will ask you a few questions, answer as below
    • enter project name (used for Trac page title and description)
    • database connection string (press Enter for default)
    • repository type (Enter for default)
    • path to repository: C:\Repositories\<ProjectName>
  • To make a single user an administrator run: trac-admin C:\Trac\<ProjectName> permission add <UserName> TRAC_ADMIN
  • Permissions for other users can be set in Trac itself, through the Admin interface

Additional settings

  • Change the Trac environment logo
    • Copy new image to: C:\Program Files\VisualSVN Server\bin\python\Lib\site-packages\trac\htdocs
    • Edit C:\Trac\<ProjectName>\conf\trac.ini, and find [header_logo] section
    • Change src to: src = common/image.jpg
  • Change the default attachment size
    • Edit C:\Trac\<ProjectName>\conf\trac.ini, and find [attachment] section
    • Change max_size = 262144 to max_size = 2097152
  • Use drop-down field instead of text box when defining ticket owner
    • This is very useful setting for smaller teams - to display all team members in a drop-down instead of typing the name in text box
    • Edit C:\Trac\<ProjectName>\conf\trac.ini, and find [ticket] section
    • Change restrict_owner = false to restrict_owner = true
    • Note that the user will not be displayed in the drop-down until he got a session on the server. The easiest way to get the session is to log in and change user preferences.

This post is more a note to myself. Other configuration options for Trac can be found in TracGuide.

Second life (of this blog)

It's been a long long time since my first and last post on this blog. I'm learning some pretty interesting stuff lately, and I've decided to write about them here. We'll see how long it will last this time. Hopefully more than a few posts.

But first of all, let me write a follow-up to my previous post called Open Source troubles. Few months ago, I've decided to take another shot on installing Trac, together with two colleagues of mine. We spent almost two working days on it, but managed to get it working on one PC with all expected features (Apache server, HTTPS, integrated with Subversion...). When we tried to repeat the process on our server, new problems aroused. Just few minutes before loosing our mind, we managed to find a lifesaver: VisualSVN Server Trac Integration.

We had already used VisualSVN Server as our Subversion server for quite some time. It's extremely easy to install and use, has a nice management console and can be configured to use HTTPS during installation. The Trac Integration package they prepared, makes the Trac installation smooth and painless. With the help of integration page, Trac + SVN server can be installed in a matter of minutes instead of days.

One thing keeps bothering me. There is no link to this integration page anywhere on VisualSVN site. I'm sure it deserves at least a small link on the bottom of VisualSVN Server page, saying "if you use Trac, take a look here for an integration option", or something like that. Instead, there are few discussions on VisualSVN Google Group mentioning it.

So, if you do like Trac or use VisualSVN Server, try this integration.