Architecture d’oignon, unité de travail et modèle générique de référentiel

C’est la première fois que j’implémente une approche de conception davantage axée sur les domaines. J’ai décidé d’essayer l’ architecture d’oignon car elle se concentre sur le domaine plutôt que sur l’infrastructure / les plates-formes / etc.

entrez la description de l'image ici

Afin de faire abstraction de Entity Framework, j’ai créé un référentiel générique avec une implémentation d’ unité de travail .

Les IRepository et IUnitOfWork :

 public interface IRepository { void Add(T item); void Remove(T item); IQueryable Query(); } public interface IUnitOfWork : IDisposable { void SaveChanges(); } 

IRepository Entity Framework de IRepository et IUnitOfWork :

 public class EntityFrameworkRepository : IRepository where T : class { private readonly DbSet dbSet; public EntityFrameworkRepository(IUnitOfWork unitOfWork) { var entityFrameworkUnitOfWork = unitOfWork as EntityFrameworkUnitOfWork; if (entityFrameworkUnitOfWork == null) { throw new ArgumentOutOfRangeException("Must be of type EntityFrameworkUnitOfWork"); } dbSet = entityFrameworkUnitOfWork.GetDbSet(); } public void Add(T item) { dbSet.Add(item); } public void Remove(T item) { dbSet.Remove(item); } public IQueryable Query() { return dbSet; } } public class EntityFrameworkUnitOfWork : IUnitOfWork { private readonly DbContext context; public EntityFrameworkUnitOfWork() { this.context = new CustomerContext();; } internal DbSet GetDbSet() where T : class { return context.Set(); } public void SaveChanges() { context.SaveChanges(); } public void Dispose() { context.Dispose(); } } 

Le référentiel client :

 public interface ICustomerRepository : IRepository { } public class CustomerRepository : EntityFrameworkRepository, ICustomerRepository { public CustomerRepository(IUnitOfWork unitOfWork): base(unitOfWork) { } } 

Contrôleur ASP.NET MVC utilisant le référentiel:

 public class CustomerController : Controller { UnityContainer container = new UnityContainer(); public ActionResult List() { var unitOfWork = container.Resolve(); var customerRepository = container.Resolve(); return View(customerRepository.Query()); } [HttpPost] public ActionResult Create(Customer customer) { var unitOfWork = container.Resolve(); var customerRepository = container.Resolve();; customerRepository.Add(customer); unitOfWork.SaveChanges(); return RedirectToAction("List"); } } 

Injection de dépendance avec unité:

 container.RegisterType(); container.RegisterType(); 

Solution:

entrez la description de l'image ici

PROBLÈMES?

  • L’implémentation de référentiel (code EF) est très générique. Tout cela fait partie de la EntityFrameworkRepository . Les référentiels de modèles concrets ne contiennent aucune de ces logiques. Cela me évite d’écrire des tonnes de code redondant, mais sacrifie-t-il éventuellement la flexibilité?

  • Les classes ICustomerRepository et CustomerRepository sont essentiellement vides. Ils sont purement là pour fournir de l’abstraction. Autant que je sache, cela correspond à la vision de l’architecture Onion, selon laquelle le code dépendant de l’infrastructure et de la plate-forme se trouve à l’extérieur de votre système, mais avoir des classes vides et des interfaces vides vous semble inacceptable

  • Pour utiliser une implémentation de persistance différente (par exemple Azure Table Storage), vous devez créer une nouvelle classe CustomerRepository et en hériter un AzureTableStorageRepository . Mais cela pourrait conduire à un code redondant (plusieurs référentiels clients)? Comment cet effet se moquerait-il?

  • Une autre implémentation (Azure Table Storage, par exemple) a des limites en matière de prise en charge transnationale. Par conséquent, la classe AzureTableStorageUnitOfWork ne fonctionne pas dans ce contexte.

Y a-t-il d’autres problèmes avec la façon dont j’ai procédé?

(J’ai pris l’essentiel de mon inspiration de ce post )

Je peux dire que ce code est assez bon pour la première fois mais il a encore des points à améliorer.

Passons en revue certains d’entre eux.

1. Injection de dépendance (DI) et utilisation de l’IoC.

Vous utilisez la version la plus simple du modèle Service Locator – instance de container elle-même.

Je suggère que vous utilisiez «injection constructeur». Vous pouvez trouver plus d’informations ici (ASP.NET MVC 4 Dependency Injection) .

 public class CustomerController : Controller { private readonly IUnitOfWork unitOfWork; private readonly ICustomerRepository customerRepository; public CustomerController( IUnitOfWork unitOfWork, ICustomerRepository customerRepository) { this.unitOfWork = unitOfWork; this.customerRepository = customerRepository; } public ActionResult List() { return View(customerRepository.Query()); } [HttpPost] public ActionResult Create(Customer customer) { customerRepository.Add(customer); unitOfWork.SaveChanges(); return RedirectToAction("List"); } } 

2. Portée de l’unité de travail (UoW).

Je ne trouve pas le style de vie de IUnitOfWork et ICustomerRepository . Je ne connais pas Unity, mais msdn indique que TransientLifetimeManager est utilisé par défaut . Cela signifie que vous obtiendrez une nouvelle instance chaque fois que vous résolvez le type.

Donc, le test suivant échoue:

 [Test] public void MyTest() { var target = new UnityContainer(); target.RegisterType(); target.RegisterType(); //act var unitOfWork1 = target.Resolve(); var unitOfWork2 = target.Resolve(); // assert // This Assert fails! unitOfWork1.Should().Be(unitOfWork2); } 

Et je m’attends à ce que l’instance de UnitOfWork dans votre contrôleur diffère de l’instance de UnitOfWork dans votre référentiel. Parfois, cela peut entraîner des bugs. Mais il n’est pas mis en évidence dans ASP.NET MVC 4 Dependency Injection comme un problème pour Unity.

Dans Castle Windsor, le mode de vie PerWebRequest est utilisé pour partager la même instance de type dans une seule requête http.

C’est une approche courante lorsque UnitOfWork est un composant PerWebRequest . Custom ActionFilter peut être utilisé afin d’appeler Commit() lors de l’appel de la OnActionExecuted() .

Je voudrais également renommer la méthode SaveChanges() et l’appeler simplement Commit comme il est appelé dans l’ exemple et dans la PoEAA .

 public interface IUnitOfWork : IDisposable { void Commit(); } 

3.1. Dépendances sur les repositorys.

Si vos référentiels vont être ‘vides’, il n’est pas nécessaire de créer des interfaces spécifiques pour eux. Il est possible de résoudre IRepository et d’avoir le code suivant dans votre contrôleur

 public CustomerController( IUnitOfWork unitOfWork, IRepository customerRepository) { this.unitOfWork = unitOfWork; this.customerRepository = customerRepository; } 

Il y a un test qui le teste.

 [Test] public void MyTest() { var target = new UnityContainer(); target.RegisterType, CustomerRepository>(); //act var repository = target.Resolve>(); // assert repository.Should().NotBeNull(); repository.Should().BeOfType(); } 

Mais si vous souhaitez avoir des référentiels qui sont “couche d’abstraction sur la couche de mappage où le code de construction de requête est concentré”. ( PoEAA, référentiel )

Un référentiel sert de médiateur entre les couches de mappage de domaine et de données, agissant comme une collection d’objects de domaine en mémoire. Les objects client construisent les spécifications de requête de manière déclarative et les soumettent au référentiel pour obtenir satisfaction.

3.2. Héritage sur EntityFrameworkRepository.

Dans ce cas, je IRepository un simple IRepository

 public interface IRepository { void Add(object item); void Remove(object item); IQueryable Query() where T : class; } 

et son implémentation qui sait comment travailler avec l’infrastructure EntityFramework et peut être facilement remplacée par une autre (par exemple AzureTableStorageRepository ).

 public class EntityFrameworkRepository : IRepository { public readonly EntityFrameworkUnitOfWork unitOfWork; public EntityFrameworkRepository(IUnitOfWork unitOfWork) { var entityFrameworkUnitOfWork = unitOfWork as EntityFrameworkUnitOfWork; if (entityFrameworkUnitOfWork == null) { throw new ArgumentOutOfRangeException("Must be of type EntityFrameworkUnitOfWork"); } this.unitOfWork = entityFrameworkUnitOfWork; } public void Add(object item) { unitOfWork.GetDbSet(item.GetType()).Add(item); } public void Remove(object item) { unitOfWork.GetDbSet(item.GetType()).Remove(item); } public IQueryable Query() where T : class { return unitOfWork.GetDbSet(); } } public interface IUnitOfWork : IDisposable { void Commit(); } public class EntityFrameworkUnitOfWork : IUnitOfWork { private readonly DbContext context; public EntityFrameworkUnitOfWork() { this.context = new CustomerContext(); } internal DbSet GetDbSet() where T : class { return context.Set(); } internal DbSet GetDbSet(Type type) { return context.Set(type); } public void Commit() { context.SaveChanges(); } public void Dispose() { context.Dispose(); } } 

Et maintenant, CustomerRepository peut être un proxy et s’y référer.

 public interface IRepository where T : class { void Add(T item); void Remove(T item); } public abstract class RepositoryBase : IRepository where T : class { protected readonly IRepository Repository; protected RepositoryBase(IRepository repository) { Repository = repository; } public void Add(T item) { Repository.Add(item); } public void Remove(T item) { Repository.Remove(item); } } public interface ICustomerRepository : IRepository { IList All(); IList FindByCriteria(Func criteria); } public class CustomerRepository : RepositoryBase, ICustomerRepository { public CustomerRepository(IRepository repository) : base(repository) { } public IList All() { return Repository.Query().ToList(); } public IList FindByCriteria(Func criteria) { return Repository.Query().Where(criteria).ToList(); } } 

Le seul inconvénient que je constate est que vous dépendez beaucoup de votre outil IOC, assurez-vous donc que votre implémentation est solide. Cependant, ce n’est pas unique aux conceptions d’oignon. J’ai utilisé Onion sur un certain nombre de projets et je n’ai jamais rencontré de “piège” réel.

Je vois quelques problèmes sérieux dans le code.

Le premier problème est la relation entre les référentiels et UoW.

  var unitOfWork = container.Resolve(); var customerRepository = container.Resolve(); 

Voici la dépendance implicite. Le référentiel ne fonctionnera pas sans UoW! Tous les référentiels ne doivent pas nécessairement être connectés à UoW. Par exemple, qu’en est-il des procédures stockées? Vous avez une procédure stockée et vous la cachez derrière le référentiel. L’invocation de procédure stockée utilise une transaction séparée! Du moins pas dans tous les cas. Donc, si je résous le seul référentiel et ajoute un élément, cela ne fonctionnera pas. De plus, ce code ne fonctionnera pas si je configure la licence temporaire Transient Life, car le référentiel aura une autre instance UoW. Nous avons donc un couplage implicite étroit.

Le deuxième problème est de créer un couplage étroit entre le moteur du conteneur DI et de l’utiliser comme localisateur de services! Le localisateur de service n’est pas une bonne approche pour mettre en œuvre l’IoC et l’agrégation. Dans certains cas, c’est anti-modèle. Le contenant DI doit être utilisé