La réponse générale est "Celui qui instancie l'ISession
devrait le disposer. Si la transaction n'a pas été validée, il s'agit en effet d'un rollback."
J'ai eu du succès en utilisant le modèle de commande pour définir une opération que je veux effectuer sur une unité de travail. Disons que nous avons une entité Personne
et l'une des choses que nous pouvons faire est de changer le nom d'une personne. Commençons par l'entité :
public class Personne
{
public virtual int Id { get; private set; }
public virtual string Nom { get; private set; }
public virtual void ChangerNom(string nouveauNom)
{
if (string.IsNullOrWhiteSpace(nouveauNom))
{
throw new DomainException("Le nom ne peut pas être vide");
}
if (nouveauNom.Length > 20)
{
throw new DomainException("Le nom ne peut pas dépasser 20 caractères");
}
this.Nom = nouveauNom;
}
}
Définissez une simple commande POCO comme ceci :
public class CommandeChangerNom : IDomainCommand
{
public CommandeChangerNom(int idPersonne, string nouveauNom)
{
this.IdPersonne = idPersonne;
this.NouveauNom = nouveauNom;
}
public int IdPersonne { get; set; }
public string NouveauNom { get; set; }
}
...et un gestionnaire pour la commande :
public class GestionnaireCommandeChangerNom : IHandle
{
ISession session;
public GestionnaireCommandeChangerNom(ISession session)
{
// Vous pourriez demander un IPersonneRepository au lieu d'utiliser directement la session.
this.session = session;
}
public void Handle(CommandeChangerNom commande)
{
var personne = session.Load(commande.IdPersonne);
personne.ChangerNom(commande.NouveauNom);
}
}
L'objectif est que le code qui existe en dehors d'une portée Session/Work puisse faire quelque chose comme ceci :
public class CertaineClasse
{
ICommandInvoker invoker;
public CertaineClasse(ICommandInvoker invoker)
{
this.invoker = invoker;
}
public void FaireQuelqueChose()
{
var commande = new CommandeChangerNom(1, "asdf");
invoker.Invoke(commande);
}
}
L'invocation de la commande implique "effectuer cette commande sur une unité de travail." C'est ce que nous voulons qu'il se passe lorsque nous invoquons la commande :
- Commencer une portée imbriquée IoC (la portée "Unit of Work")
- Démarrer une ISession et une Transaction (cela est probablement implicite dans le cadre de l'étape 3)
- Résoudre un
IHandle
de la portée IoC
- Passer la commande au gestionnaire (le domaine fait son travail)
- Valider la transaction
- Fin de la portée IoC (l'Unité de Travail)
Voici un exemple utilisant Autofac comme conteneur IoC :
public class InvocateurUnitOfWork : ICommandInvoker
{
Autofac.ILifetimeScope scope;
public InvocateurUnitOfWork(Autofac.ILifetimeScope scope)
{
this.scope = scope;
}
public void Invoke(TCommand commande) where TCommand : IDomainCommand
{
using (var porteeTravail = scope.BeginLifetimeScope("UnitOfWork")) // étape 1
{
var gestionnaire = porteeTravail.Resolve>(); // étape 3 (implique l'étape 2)
gestionnaire.Handle(commande); // étape 4
var session = porteeTravail.Resolve();
session.Transaction.Commit(); // étape 5
} // étape 6 - Lorsque la "porteeTravail" est disposée, Autofac disposera l'ISession.
// Si une exception était levée avant la validation, la transaction est annulée.
}
}
Remarque : L'InvocateurUnitOfWork
que j'ai montré ici viole le SRP - il s'agit d'une UsineUnitOfWork
, d'un UnitOfWork
, et d'un Invokeur
tout en un. Dans mon implémentation réelle, je les ai séparés.