Данное руководство устарело. Актуальное руководство: Руководство по ASP.NET Core
Термин "Onion Architecture" ("луковая" архитектура) был предложен Джеффри Палермо (Jeffrey Palermo) еще в 2008 году. Спустя годы данная концепция стала довольно популярной и является одной из наиболее применяемых типов архитектуры при построении приложения на ASP.NET.
Onion-архитектура представляет собой разделение приложения на уровни. При чем есть один независимый уровень, который находится в центре архитектуры. От этого уровня зависит второй уровень, от второго - третий и так далее. То есть получается, что вокруг первого независимого уровня наслаивается второй-зависимый. Вокруг второго наслаивается третий, который также может зависеть и от первого. Образно это может быть выражено в виде лука, в котором также есть сердцевина, вокруг которого наслаиваются все остальные слои, вплоть до шелухи.
Количество уровней может отличаться, но в центре всегда находится модель домена (Domain Model), то есть те классы моделей, которые используются в приложении и объекты которых хранятся в базе данных:
Первый уровень вокруг модели домена образуют интерфейсы, которые управляют работой с моделью домена. Обычно это интерфейсы репозиториев, через которые мы взаимодействуем с базой данных.
Внешний уровень представляет такие компоненты, которые очень часто изменяются. Обычно внешний уровень образуют пользовательский интерфейс, тесты, какие-то вспомогательные классы инфраструктуры приложения. К этому уровню также относятся конкретные реализации интерфейсов, объявленных на нижележащих уровнях. Например, реализация интерфейса репозитория, который объявлен на уровне Domain Services. Вообще все внутрение уровни, которые можно объединить в Application Core, определяют только интерфейсы, а конкретная реализация этих интерфейсов располагается на внешнем уровне.
Также стоит отметить, что все внешние хранилища, как базы данных, файлы, внешние веб-сервисы, от которых мы можем получать данные, - все это является внешним по отношению к архитектуре.
Для более подробного рассмотрения данного типа архитектуры создадим обычный проект ASP.NET MVC 5, который будет называться OnionApp.
Пока это монолитное приложение, в котором весь код размещен в одном проекте. Теперь добавим в решение (не в проект) новую папку. Назовем ее Domain. Затем добавим в папку новый проект. В качестве типа проекта выберем тип Class Library, а в качестве его названия укажем OnionApp.Domain.Core:
Добавим в новый проект класс, представляющий книгу, который и будут представлять Domain Model:
namespace OnionApp.Domain.Core { public class Book { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } } }
Затем добавим в папку Domain новый проект также по типу Class Library, а в качестве его названия укажем OnionApp.Domain.Interfaces.
Затем добавим в этот проект ссылку на вышеопределенный проект OnionApp.Domain.Core
и также добавим новый интерфейса:
using System; using System.Collections.Generic; using OnionApp.Domain.Core; namespace OnionApp.Domain.Interfaces { public interface IBookRepository: IDisposable { IEnumerable<Book> GetBookList(); Book GetBook(int id); void Create(Book item); void Update(Book item); void Delete(int id); void Save(); } }
Этот интерфейс и составляет уровень Domain Services и зависит от уровня Domain Model.
И на данный момент проекты выглядят так:
При создании архитектуры приложения надо понимать, что реальное количество уровней здесь весьма условно. В зависимости от масштаба задач уровней может быть и больше, и меньше. Однако важно понимать сам принцип, что в центре у нас модели домена, а все остальное зависит от них. Каждый внешний уровень может зависеть от внутреннего, но не наоборот.
На выше представленной схеме между внешним уровнем и уровнем Domain Services есть еще уровень API или интерфейсов бизнес-логики приложения - уровень Application Services. Этот уровень может включать интерфейсы вспомогательных классов. Например, покупка книги может представлять собой объект, который в зависимости от способа оплата (наличкой, через кредитную карту, через электронные деньги) может включать тот или иной функционал. И, возможно, было бы неплохо определить общий интерфейс покупки, а в зависимости от типа магазина использовать его конкретную реализацию. Поэтому добавим в решение новую папку, которую назовем Services. В эту папку добавим новый проект по типу Class Library, который назовем OnionApp.Services.Interfaces
И добавим в этот проект интерфейс IOrder:
using OnionApp.Domain.Core; namespace OnionApp.Services.Interfaces { public interface IOrder { void MakeOrder(Book book); } }
Данный проект также имеет зависимость от классов проекта OnionApp.Domain.Core
. А интерфейс IOrder, представляющий процесс
покупки и оформления заказа, использует эти классы в методе MakeOrder()
. Предполагается, что в метод передается объект купленной книги.
Теперь перейдем к созданию внешнего уровня, который и будет реализовывать данные интерфейсы. Для этого добавим в решение папку Infrastructure и затем в нее добавим новый проект по типу Class Library, который назовем OnionApp.Infrastructure.Data.
Данный проект будет реализовывать интерфейсы, объявленные на нижних уровнях, и связывать их с хранилищем данных. В качестве хранилища данных будет использоваться бд MS SQL Server, с которой мы будем взаимодействовать через Entity Framework. Поэтому добавим в этот проект через nuGet все пакеты Entity Framework. Также добавим в проект ссылки на проекты OnionApp.Domain.Core и OnionApp.Domain.Interfaces.
После этого добавим в проект новый класс OrderContext:
using System; using System.Collections.Generic; using System.Data.Entity; using OnionApp.Domain.Core; namespace OnionApp.Infrastructure.Data { public class OrderContext : DbContext { public DbSet<Book> Books { get; set; } } }
Также добавим класс репозитория BookRepository:
using System; using System.Collections.Generic; using System.Linq; using OnionApp.Domain.Core; using OnionApp.Domain.Interfaces; using System.Data.Entity; namespace OnionApp.Infrastructure.Data { public class BookRepository : IBookRepository { private OrderContext db; public BookRepository() { this.db = new OrderContext(); } public IEnumerable<Book> GetBookList() { return db.Books.ToList(); } public Book GetBook(int id) { return db.Books.Find(id); } public void Create(Book book) { db.Books.Add(book); } public void Update(Book book) { db.Entry(book).State = EntityState.Modified; } public void Delete(int id) { Book book = db.Books.Find(id); if (book != null) db.Books.Remove(book); } public void Save() { db.SaveChanges(); } private bool disposed = false; public virtual void Dispose(bool disposing) { if (!this.disposed) { if (disposing) { db.Dispose(); } } this.disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } }