The general answer is "Whoever instantiates the ISession
should dispose of it. If the transaction has not been committed, this is effectively a rollback."
I've had success by using the command pattern to define an operation that I want to perform on a unit of work. Say we have a Person
entity and one of the things we can do is change a person's name. Let's start with the entity:
public class Person
{
public virtual int Id { get; private set; }
public virtual string Name { get; private set; }
public virtual void ChangeName(string newName)
{
if (string.IsNullOrWhiteSpace(newName))
{
throw new DomainException("Name cannot be empty");
}
if (newName.Length > 20)
{
throw new DomainException("Name cannot exceed 20 characters");
}
this.Name = newName;
}
}
Define a simple POCO Command like this:
public class ChangeNameCommand : IDomainCommand
{
public ChangeNameCommand(int personId, string newName)
{
this.PersonId = personId;
this.NewName = newName;
}
public int PersonId { get; set; }
public string NewName { get; set; }
}
...and a Handler for the command:
public class ChangeNameCommandHandler : IHandle<ChangeNameCommand>
{
ISession session;
public ChangeNameCommandHandler(ISession session)
{
// You could demand an IPersonRepository instead of using the session directly.
this.session = session;
}
public void Handle(ChangeNameCommand command)
{
var person = session.Load<Person>(command.PersonId);
person.ChangeName(command.NewName);
}
}
The goal is that code that exists outside of a Session/Work scope can do something like this:
public class SomeClass
{
ICommandInvoker invoker;
public SomeClass(ICommandInvoker invoker)
{
this.invoker = invoker;
}
public void DoSomething()
{
var command = new ChangeNameCommand(1, "asdf");
invoker.Invoke(command);
}
}
The invocation of the command implies "do this command on a unit of work." This is what we want to happen when we invoke the command:
- Begin an IoC nested scope (the "Unit of Work" scope)
- Start an ISession and Transaction (this is probably implied as part of step 3)
- Resolve an
IHandle<ChangeNameCommand>
from the IoC scope
- Pass the command to the handler (the domain does its work)
- Commit the transaction
- End the IoC scope (the Unit of Work)
So here's an example using Autofac as the IoC container:
public class UnitOfWorkInvoker : ICommandInvoker
{
Autofac.ILifetimeScope scope;
public UnitOfWorkInvoker(Autofac.ILifetimeScope scope)
{
this.scope = scope;
}
public void Invoke<TCommand>(TCommand command) where TCommand : IDomainCommand
{
using (var workScope = scope.BeginLifetimeScope("UnitOfWork")) // step 1
{
var handler = workScope.Resolve<IHandle<TCommand>>(); // step 3 (implies step 2)
handler.Handle(command); // step 4
var session = workScope.Resolve<NHibernate.ISession>();
session.Transaction.Commit(); // step 5
} // step 6 - When the "workScope" is disposed, Autofac will dispose the ISession.
// If an exception was thrown before the commit, the transaction is rolled back.
}
}
Note: The UnitOfWorkInvoker
I've shown here is violating SRP - it is a UnitOfWorkFactory
, a UnitOfWork
, and an Invoker
all in one. In my actual implementation, I broke them out.