La respuesta general es "Quien crea la instancia ISession
debe deshacerse de ella. Si la transacción no se ha confirmado, esto es efectivamente una reversión".
He tenido éxito usando el patrón de comando para definir una operación que quiero realizar en una unidad de trabajo. Digamos que tenemos una entidad Person
y una de las cosas que podemos hacer es cambiar el nombre de una persona. Vamos a empezar con la entidad:
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;
}
}
Definir un simple comando POCO así:
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; }
}
... y un controlador para el comando:
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);
}
}
El objetivo es que el código que existe fuera de un alcance de sesión/trabajo puede hacer algo como esto:
public class SomeClass
{
ICommandInvoker invoker;
public SomeClass(ICommandInvoker invoker)
{
this.invoker = invoker;
}
public void DoSomething()
{
var command = new ChangeNameCommand(1, "asdf");
invoker.Invoke(command);
}
}
La invocación del comando implica "hacer este comando en una unidad de trabajo"."Esto es lo que queremos que suceda cuando se invoca el comando:
- Comenzar un ámbito anidado COI (la 'unidad de trabajo' alcance)
- inicio de una ISession y transacción (esto probablemente se da a entender en el marco de paso 3)
- resolver un
IHandle<ChangeNameCommand>
del ámbito
- COI Pasar el comando al controlador (el dominio hace su trabajo)
- confirmar la transacción
- Terminar el alcance de la COI (la unidad de trabajo)
Así que aquí es un ejemplo usando Autofac como el contenedor IoC:
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.
}
}
Nota: El UnitOfWorkInvoker
que he mostrado aquí está violando SRP - es un UnitOfWorkFactory
, un UnitOfWork
, y un Invoker
todo en uno . En mi implementación real, los eché.
Ayende tiene una aplicación de ejemplo https://github.com/ayende/Effectus –
ok que podría ser una solución para formularios win ... ¿qué pasa con los servicios de Windows? – Rookian
Puedo describir un patrón con el que he tenido éxito. Se aplica si tiene un modelo simple de "una transacción por operación" (no se requieren transacciones anidadas) y está utilizando un contenedor IoC. ¿Esto satisfaría tus necesidades? Básicamente, cuando el código de la capa de servicio decide que es hora de "hacer trabajo de dominio", usa el patrón de comando y un invocador para el comando (el IoC le da el invocador al código de servicio) –