ASP.NET MVC: История одного проекта "Всё ради данных" (часть 2)
Сайтостроение | создано: 28.04.2012 | опубликовано: 28.04.2012 | обновлено: 13.01.2024 | просмотров: 187390 | всего комментариев: 62
После некоторого раздумья, решил переименовать статьи, чтобы название максимально подходило к теме статьи. Я просто расскажу как я создаю себе новый сайт.
Содержание
ASP.NET MVC: История одного проекта "Готовимся к старту" (часть 1)
ASP.NET MVC: История одного проекта "Всё ради данных" (часть 2)
ASP.NET MVC: История одного проекта "Шаблоны и внешний вид" (часть 3)
ASP.NET MVC: История одного проекта "Еще немного классов" (часть 4)
ASP.NET MVC: История одного проекта "UI - всё для пользователя" (часть 5)
ASP.NET MVC: История одного проекта "UI - Добавление экспоната" (часть 6)
ASP.NET MVC: История одного проекта "UI - Редактирование экспоната" (часть 7)
ASP.NET MVC: История одного проекта "Обработка ошибок" (часть 8)
ASP.NET MVC: История одного проекта "Фильтрация" (часть 9)
ASP.NET MVC: История одного проекта "Поиск" (часть 10)
ASP.NET MVC: История одного проекта "Облако тегов" (часть 11)
ASP.NET MVC: История одного проекта "Главная страница" (часть 12)
Итоги прошлой части
Выдалась свободная минутка, продолжу писать свой сайт. Итак, в предыдущей части были решены основные задачи, которые всегда приходится решать в начале разработки. Запустим сайт, проверим что работает авторизация.
Отлично. Давайте перейдем к моделям.
Откуда беруться данные
Не могу сказать за всех разработчиков мира, только сугубо личные предпочтения относительно того, на каком этапе нужно заносить в базу данных "временные" данные, а на каком "реальные". Я сторонник голой, но правды... хреновой, но реальности! На первом этапе я покажу как создать контроллер, представления и репозитории что называется "вручную". Для этого я буду использовать класс Hall. А вот для реализацию контроллера (controller), преставления (view) и репозитория (repository) для класса Exhibit я покажу на примере MvcScaffolding. Сам паттерн MVC для ASP.NET описывать не имеет смысла, потому что лучшие умы человечества уже сделали это, например, в Википедии.
Как пройти в хранилище?
Прежде чем начать работу над первой (нудной) частью оглашенной в предыдущем абзаце, надо бы подумать о контейнере...О UnityContainer конечно же. Подключим через nuget-пакет Unity.Mvc3.
PM> Install-Package Unity.Mvc3 Attempting to resolve dependency 'Unity (? 2.1.505.0)'. Attempting to resolve dependency 'CommonServiceLocator (? 1.0)'. Successfully installed 'CommonServiceLocator 1.0'. You are downloading Unity from Microsoft patterns & practices, the license agreement to which is available at http://www.opensource.org/licenses/ms-pl. Check the package for additional dependencies, which may come with their own license agreement(s). Your use of the package and dependencies constitutes your acceptance of their license agreements. If you do not accept the license agreement(s), then delete the relevant components from your device. Successfully installed 'Unity 2.1.505.0'. Successfully installed 'Unity.Mvc3 1.1'. Successfully added 'CommonServiceLocator 1.0' to Calabonga.Mvc.Humor. Successfully added 'Unity 2.1.505.0' to Calabonga.Mvc.Humor. Successfully added 'Unity.Mvc3 1.1' to Calabonga.Mvc.Humor. PM>
Теперь надо в файле Global.asax в методе Application_Start подключить инициализацию контейнера.
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); Bootstrapper.Initialise(); }
Вот теперь всё готово, чтобы начать реализовывать контроллер, представления и репозитории. Я создал в проекте новую папку Engine, в которую и буду складывать файлы с классами описывающие инфраструктуру.
А как же данные?
Создаю новый класс MuseumContext как наследник от DbContext добавляю таблицу для хранение моих залов.
/// <summary> /// База данных /// </summary> public class MuseumContext : DbContext { /// <summary> /// Таблица залов /// </summary> public DbSet<Hall> Halls { get; set; } }
Как, наверное вы уже догадались, это и есть подход к реализацию базы данных именуемый "Code First", что означает "сначала код". Я сначала создал классы и специальный контекст. Сейчас в web.config пропишу строку подключения и мой сайт будет готов работать с данными.
Строка подключения
Магические силы EntityFramework 4.3 позаботятся о моих данных, но для того чтобы это произошло надо написать заклинание правильную строку подключения. Правильность соблюдается следующим образом, название моего контекста MuseumContext должно соответствовать названию строки подключения. Это придумал не я, это требование EntityFramework.
<add name="MuseumContext" connectionString="Data Source=(local);Initial Catalog=museumDb;Integrated Security=True" providerName="System.Data.SqlClient" />
Одно важное замечание. Даже если сейчас вы и запустите сайт на компиляцию, база данных не появится в списке Microsoft SQL Managment Studio (делее буду назвать SQL MS), но при первом обращении к таблицам MuseumContext сам EF сгенерирует для вас базу данных. А вот чтобы это происходило с максимальной пользой, давайте скажем EF чтобы он не просто генерировал пустые таблицы, а еще и наполнял их некоторыми данными. Раз я завел речь про залы музея, пусть названия залов и наполняюся сразу при генерации. Для этого подключим миграции к EF выполнив в консоле nuget-менеджера простую но очень важную команду (надо сказать, что эта команда появилась только в версии 4.3.):
PM> Enable-Migrations Code First Migrations enabled for project Calabonga.Mvc.Humor. PM>
Миграции (Migrations Code First)
После выполнения команды в проекте появилась папка Migrations, в которой появился файл Configuration.cs (студия его любезно открыла). Я поправил конструктор:
public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; }
Надеюсь, названия параметров говоря сами за себя и комментировать мне их не придется. А в методе Seed я написал инциализацию моей таблицы Hall:
protected override void Seed(MuseumContext context) { context.Halls.AddOrUpdate( new Hall { Id = 1, Name = "Анекдоты" }, new Hall { Id = 2, Name = "Истории" }, new Hall { Id = 3, Name = "Афоризмы" }, new Hall { Id = 4, Name = "Стишки" }, new Hall { Id = 5, Name = "Хокку" }, new Hall { Id = 6, Name = "Фразы и изречения" }, new Hall { Id = 7, Name = "Разное" } ); }
Обратите внимание на то, что идентификаторы тоже проставлены, если их не указывать, то EF будет добавлять вышеуказанные категории при каждом запуске сайта (горький опыт разработки). На этом работа с файлом конфигурации не закончена, надо ее (конфигурации) еще и подключить. Идем в global.asax и в метод Application_Start дописываем код:
protected void Application_Start() { DbMigrator migrator = new DbMigrator(new Configuration()); migrator.Update(); AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); Bootstrapper.Initialise(); }
Это указан первый способ подключения конфигурации (миграции). Этот способ прост и выполняется автоматически при старте системы. В релизной версии проекта, код рекомендуется убрать. Второй способ - запуск на выполнение из консоли nuget-менеджера метод Update-Database "вручную". Выбирайте сами, какой способ вам нравится больше. Скажу что, и тот и другой способ имеют свои возможности, а также плюсы и минусы, о которых я возможно расскажу по ходу написания статей.
Огласите весь список, пожалуйста, или откуда брать классы
Пришло время увидеть данные, о которых мы так успешно поговорили. Создаем репозиторий HallRepository. Я создал файл, в который поместил класс и интерфейс:
public class HallRepository: IHallRepository { } public interface IHallRepository { IQueryable<Hall> All { get; } IQueryable<Hall> AllIncluding(params Expression<Func<Hall, object>>[] includeProperties); Hall Find(int id); void Insert(Hall district); void Update(Hall district); void Delete(int id); void Save(); }
Теперь, определившись с интерфейсом, начнем его реализацию в классе... Опаньки! Не тут-то было! Для реализации интерфейса мне требуется доступ к базе данных (к MuseumContext)... Запихнем контекст в контейнер, чтобы можно было получить его через Dependency Injection в конструкторе репозитории. Для этого в Bootstrapper в методе инициализации контейнера (BuildUnityContainer) зарегистрируем MuseumContext:
container.RegisterType<MuseumContext>(new HierarchicalLifetimeManager());
Хотелось немного остановиться и оговорить этот способ регистрации, но не буду, ибо он выявлен как самый оптимальный, потому что контекст создается на поток (на один запрос), новый каждый раз, а это как раз то, что нужно (горький опыт разработки). Просто поверьте и всё.
В контрукторе HallRepository попробуем получить экземпляр контекста:
private readonly MuseumContext context; public HallRepository(MuseumContext context) { this.context = context; }
А для того чтобы это сделать надо и IHallRepository поместить в контейнер:
container.RegisterType<IHallRepository, HallRepository>();
Я добавил эту строку после регистрации контекста, чего и вам советую, потому что сначала должен быть зарегистрирован контекст, а потом уже репозитории его использующие.
Чтобы проверить работоспособность контейнера и Dependency Injection надо в конструкторе какого-нибудь контроллера попробовать получить наш новый репозиторий. Я попробую сделать это в HomeController:
public class HomeController : Controller { private readonly IHallRepository hallRepository; public HomeController(IHallRepository halls) { this.hallRepository = halls; } public ActionResult Index() { ViewBag.Message = "Welcome to ASP.NET MVC!"; return View(); } public ActionResult About() { return View(); } }
Ставлю breakpoint на конструктор репозитория и контроллера и запускаю... За одно посмотрим, будет ли создана база автоматически. УРА!!! Работает! Репозиторий получил контекст:
А контроллер получил репозиторий:
И даже база данных museumDb успешно создана:
То что доктор прописал! Полдела, можно сказать, сделано! Основная рутина почти закончилась. Теперь надо написать реализацию интерфейса в классе HallRepository и можно выводить список залов (Hall) на представление (View) на просмотр!
public IQueryable<Hall> All { get { return context.Halls; } } public IQueryable<Hall> AllIncluding(params Expression<Func<Hall, object>>[] includeProperties) { IQueryable<Hall> query = context.Halls.OrderBy(x => x.Name); foreach (var includeProperty in includeProperties) { query = query.Include(includeProperty); } return query; } public Hall Find(int id) { return context.Halls.Find(id); } public void Insert(Hall hall) { context.Halls.Add(hall); } public void Update(Hall hall) { Hall h = this.Find(hall.Id); h.Name = hall.Name; Save(); } public void Delete(int id) { Hall h = this.Find(id); if (h != null) {c ontext.Halls.Remove(h); Save(); } } public void Save() { context.SaveChanges(); }
Теперь я могу в контролле HomeController получить список залов, и даже показать их на Index.
и код в контроллере:
public ActionResult Index() { return View(hallRepository.All); }
запускаем...
О, это чудо! Контейнер, репозитории, EF и всё работает! Да еще и как надо!
На этом пока всё. Продолжение, как говориться, следует. Дальше будет еще интереснее. Пишите комментарии.
Комментарии к статье (62)
для Artyom Krivokrisenko
"...Проблема книжки Эспозито (да и почти всех книжек и статей, которые я видел по MVC) в том что их задача - демонстрация технологии, а не хорошего патерна..." - это сильно сказано! Целиком и полностью поддерживаю!
И про фразы в статьях я тоже понял, это действительно дельный совет, спасибо. А про базы я же написал почему я так сделал! Это только момент разработки, да и то пока будут модели приводятся к окончательному виду и структуре. Чтобы пользователя с правами админа не заводить каждый раз после удаления базы. А базу удалять чтобы в процессе разработки (черновой) не писать миграции.
Всё равно спасибо, я думаю каждый сделает выводы из прочитанного.
EF и Unity не нужны
hazzik, а что нужно? почему не нужны?
Не стоит выставлять наружу IQueryable. В итоге может выйти что запрос для получения данных пойдет уже после уничтожения контекста бд.
Andy, вынужден с вами не согласиться. Именно по принципу "выставить наружу IQueryable" и работают Ria-services, да и Web API сервисы, да и в MVC 4 появляется ApiController, который работает по этому же принципу. А гарантировать уничтожение контекста в "правильное время" как раз и помогает UnityContainer (я описал HierarchicalLifetimeManager, что самый оптимальный проверенный вариант, во всяком случае для меня, а в более простых проектах достаточно даже PerThreadLifetimeManager).
Artyom Krivokrisenko, действительно вы правы относительно Unity особенно в концнции MVC. Я выдал такое предупреждение, потому что при разработке под Windows Phone есть некоторые нюансы (хотя я и использовал Funq контейнер). Но, согласитесь, это ни коим образом не помешает, а только добавит понимания и определенности. :)
Давайте начнем с чтого, зачем вам тут IQueryable<T> по сравнению с IEnumerable<T>.
Andy, достаточно всего лишь того, что это IEnumerable<T> возращает реальные данные, а IQueryable<T> возращает всего лишь сформированный запрос к SQL серверу... Хотя конечно же всё зависит от сложности запроса (связи по свойствам навигации).
А что если одному контроллеру понадобится несколько репозиториев? Не лучше ли их сразу инкапсулировать в unit of work? И еще, на мой взгляд, лучше в контроллерах не лезть к репозиториям, а сделать еще отдельный уровень сервисов. Все равно ведь добавится в дальнейшем множество доработок, которые надо будет вносить, и в итоге получится "bloat controller".
Вот именно, т.е. по сути вы даете возможность работать почти напрямую с БД за пределами уровня DAL. Я всегда могу вызвать All() и потом прикрутить нужные параметры хоть во View. Т.е. это потенциальный ход против логики MVC.
Кроме того, я уже не говорю о сложности реализации IQueryable<T> для произвольного источника данных (это если говорить про вносимые интерфесами абстракции). Почему тогда сразу не использовать Linq2SQL ;) Все это делает репозиторий в данном случае бессмысленной надстройкой.
Кстати, в MVC4 WebAPI только дает возможность выдавать IQueryable если вам так нужен oData в сервисе. Вполне можно обойтись и без него.
Уважаемый, gothdotnet, дело в том, что как раз контроллер является основополагющим столпом в концепции фреймворка (ASP.NET MVC). Во всяком случае, так пишет в своей книга Дино Эспозито:
...In spite of the explicit reference to the Model-View-Controller pattern in the name, the ASP .NET MVC architecture is essentially centered on one pillar—the controller . The controller governs the processing of a request and orchestrates the back end of the system (for example, business layer, services, data access layer) to grab raw data for the response . Next, the controller wraps up raw data computed for the request into a valid response for the caller . When the response is a markup view, the controller relies on the view engine module to combine data and view templates and produce HTML...
Таким образом, именно контроллер по его разумению, должен получать все репозитории необхродимые для его полноценной работы. Я полностью поддерживаю эту концепцию одного из разработчиков фреймворка. :)
Andy, вы меня совершенно правильно поняли. Именно для того, чтобы в контроллере получить доступ к таблице (вернее сказать к сущностям через их репозитории). Ибо, если контроллер, по сути единственно правильное место (читайте комментарий со словами Дино Эспозито), то именно в контроллере будет доступ к DAL (через репозитории), к сервисам и ко всему, что потребуется.
О ужас. Мне очень жаль, что я прочитал эту статью и тем более комментарии к ней. То что прочитано - не может быть разпрочитано:(
Вы, вероятно, имели ввиду, говоря про доступ к данным в контроллере напрямую в репозитории:
Next, the controller wraps up raw data computed for the request into a valid response for the caller
Здесь, скорее всего, имеется ввиду построение View Models, которые будут переданы во Views из данных, которые будут получены в том числе (ну, чтобы не городить кучу переменных в ViewBag или ViewData).
Про сервисы, там же тоже упоминается:
The controller governs the processing of a request and orchestrates the back end of the system (for example, business layer, services, data access layer)
Не будет идти в разрез с советами Эспозито, если контроллер будет вызывать бизнес-методы из слоя сервисов и оборачивать полученные данные во вьюшки. По крайней мере, я не вижу противоречия (поясните, если что не так :) ).
Просто, если представить проект хотя бы среднего размера, в котором контроллер выполняет всю бизнес логику в пределах определенной области, то это наверное очень неудобно поддерживать.
То есть я говорю просто об основных принципах ООП и не вижу в этом никакого противоречия с тем, что пишет Эспозито.
Gotdotnet, безусловно, зашивать бизнес-логику в контроллер совершенно неприемлемо! Но и репозитории предназначены не для этого. Я только хотел сказать, что именно в контроллере всё перечисленное должно встречаться: и репозитории, и, так называемый, "менеджер бизнес-процессов", и провайдеры данных, и вспомогательные утилиты, например, логгеры...
Denis Gladkikh, вы знаете, что я отвечу;) Не буду тут холиварить. И да, это не потому что я не люблю M$, а потому что мне довелось поработать и с тем и с тем. Обе реализации - мягко говоря IE6 в своих областях.
Единственное приемущество этих двух реализаций - "они от M$", но сообщество .NET и сам M$ уже давно перерос эту стадию.
Одно только отсутсвие поддержки Enum в EF чего стоит, и зря они отказались от пользовательских конвенций.
Calabonga, как вы сделали такой вывод мне не ясно. Он правильно пишет что контроллер "governs the processing of a request" – именно обработка и передача запроса к business layer. А вся логика приложения находится именно в business layer. Вспомните хотябы зачем вообще вся эта заморочка с разными layer и разделением на M+V+C.
Хотя я понимаю, что большинство примеров как раз для упрощения содержат всю логику в контроллерах. Может отсюда такой вывод у вас?
hazzik, а можно узнать в чем ужас комментариев. А то такие заявления без указания сути – понт. А насчет EF – Code First отлично подходит для начала разработки приложения. А затем по необходимости (и при условии грамотного проектирования интерфейсов DAL) можно просто заменить DAL на любой другой.
Andy, именно к этому я всё и сказал... Обработка и передача...
Andy, в ваших комментариях-то, как раз, всё хорошо;)
>А насчет EF – Code First отлично подходит для начала разработки приложения.
Ага, было такое: начали быстро, а потом замучались до смерти.
>А затем по необходимости (и при условии грамотного проектирования интерфейсов DAL) можно просто заменить DAL на любой другой.
В теории можно, на практике - не возможно: EF (даже CodeFirst) очень глубоко пускает корни. К примеру, чтоб не быть голословным:
ForeignKeyAttribute выдаёт себя за жителя System.ComponentModel.DataAnnotations, а сам живет в EntityFramework.
На мой взгляд, оптимальная абстракция - это инкапсуляция запросов в QueryObject, которые принимают критерии и возвращают Dto. В этом случае, вы можете менять DAL (или как вы там его называете) сколько угодно, при этом ВООБЩЕ не меняя логику приложения. При таком подходе ни какие repository и unit-of-work не нужны.
Ну задачи переубедить у меня нет, поэтому - дело ваше. Но IMHO приложение строите вы не правильно, т.к. "как" выбирать данные, по каким условиям итд это задача бизнес-логики, а не контроллера.
hazzik я не за себя, именно за конструктив.
> Ага, было такое: начали быстро, а потом замучались до смерти.
Т.е. сильно завязали на него бизнес-логику (кстати, по неймспейсу атрибутов там изменения в .NET4.5). Но, может для больших проектов и так. Но мне для средних сайтов вполне хватает. Опять же, вместо DTO у меня практически POCO, а слой BL и выше про EF ничего не знает.
> На мой взгляд, оптимальная абстракция - это инкапсуляция запросов в QueryObject
А ссылки есть что почитать на эту тему? Суть то понятна, да и общее описание не раз встречал. А вот пример бы хороший поглядел. Может в очередном проекте попробую.
Andy, не плохо было бы писать, к кому вы обращаетесь, а то не понятно.
Автор, премодерация не нужна.
Calabonga, почитайте того же Эспозито. но книгу Architecting Applications for the Enterprise, 7-ю главу. раздел Common Pitfalls of a Presentation Layer.
We're not far from the truth if we say that user-interface facilities encourage developers to put more stuff and code in the presentation layer than it should reasonably contain. As a result, the presentation layer operates as a catch-all for the logic of other layers—mostly the middle tier, but often also the data access layer. Is this really bad? And if so, why is it all that bad? ...
Although such an approach is OK for simple applications, for large, durable, enterprise-class systems it is another story. When it comes to such types of applications, stuffing code in the presentation layer that logically belongs to other tiers is a tremendous sin.
На демо. и в примерах их книг все обычно дергают репозитории из контроллера, потому что так быстрее и проще. Но в крупных приложениях все намного суровее. Пролистайте эту книгу, и поймете, что работа с репозиториями - logically belongs to BL. Я могу нацитировать оттуда, если вы доверяете именно Эспозито :).
PashaPash, если вы найдете хоть одно упоминание в статье, где я утверждаю обратное - я буду вам очень признателен! Ибо я ни разу не сказал, что BLL должен находится в контроллере. А в статье всё упрощено для лучшего понимания.
Я понимаю что в статье все упрощено, но в комментах:
Таким образом, именно контроллер по его разумению, должен получать все репозитории необхродимые для его полноценной работы.
всё перечисленное должно встречаться: и репозитории, и, так называемый, "менеджер бизнес-процессов", и провайдеры данных, и вспомогательные утилиты, например, логгеры...
MVC - это всего-лишь presentation pattern. И он затрагивает только Presentation Layer.
Репозиторий - это интерфейс между BLL и DAL. Он не имеет никакого отношения к Presentation Layer.
Провайдеры данных - тоже явно что-то из DAL, тем более не должны быть видны в PL.
Вы просто очень вольно трактуете слова Эспозито. Он пишет что "контроллер... должен пнуть бэк-энд (например, business layer, services, data access layer) чтобы тот отдал ему данные для ответа. Вы почему-то читаете те же слова как "контроллер управляет и BL, и сервисами. и DAL". На самом деле Эспозито не захотел явно привязаться к 3-layer, и допустил что BL у вас может и не быть. На практике же почти у всех 3-layer, есть PL/BLL/DAL, и вызов DAL (методов репозитория/провайдера) из PL (контроллера) - это layer skipping, признак серьезных проблем в архитектуре.
Перечитайте книгу Эспозито по архитектуре. Набьете намного меньше шишек в будущем.
Ну что ж, PashaPash, вынужден признать, что я оговорился, а вы совершенно правы. Более того в рабочем проекте (у нас группа разработчиков) обязательным образом соблюдает перечисленные вами принципы. И, да... это "соблюдение" было включено иммено из-за шишек совершенных при проектировании архитектруры в прошлых версиях.
Еще раз хочу всех поблагодарить всех за участие в продуктивной полемике. Впредь, обязуюсь более тщательным образом следить за мыслями сложенными в слова и предложения и опубликованных в этом блоге.
Спасибо.
P.S.: надо сделать чтобы логин и мыло в кукисы сохранялись... сам устал вводить их снова и снова...
HallRepository только оборачивает работу context.Halls. Зачем нужна такая абстракция, тем более, что из репозитория возвращается IQueryable? Если отказаться от репозитория в пользу работы с контекстом напрямую, то контекст окажется в контроллере. И сразу станут очевидны все проблемы в архитекртуре.
В своих приложениях делаю так: абстрактный generic-репозиторий для выполнения CRUD операций над объектами.Затем его наследники уже типизированные репозитории для работы с конкретными сущностями и методами, содержащие какую-то бизнес-логику. Все это инкапсулирую в UOW.
В данном случае можно ли репозитории спозиционировать как BLL?
Murad, в примере указанном в статье , всю, так называемую бизнес-логику у меня несет контроллер, в силу того, что логики, как таковой, у меня нет. Но если бы была замудренная логика, то мой контроллер получал бы тогда, что-то типа HallManager, в который падал бы мой репозиторий IHallRepository через вливание. А если бы HallManager работал не только с одной сущностью, то тогда бы в этот самый манагер, падали бы все необходимые репозитории. Это как вариант.
Но в силу того что, на данный момент, Code First поддерживает только EF, то промежуточный уровень абстракции (репозитории) по большому счету, не имеет смысла. Ибо нет альтернативы, что применить абстрагирование от конкретной ORM. Поэтому, достаточно просто в этот самый HallManager "свалить" DbContext... Это тоже как варинт...
skyliver, думаю что как вариант, такой подход имеет место быть. Хотя если строго следовать шаблонам проектирования (не использовать вольные трактовки законодателей программного кода), то такой вариант противоречит концепции репозитория. Прочитайте, пожалуйста, последний комментарий от PashaPash, в котором всё разложено по полочкам.
hazzik, примерно такой же коммент я чуть было не отправил на первый ваш комментарий... Конструктивная критика есть?
>Конструктивная критика есть?
Ну, во-первых, из-за того, что предложение написано безграмотно, я полностью упустил его смысл.
Во-вторых, даже если учесть, что я понял это правильно, предложение всё равно бессмысленно.
hazzik, прошу великодушно меня простить. У меня даже высшего образования нет, может я поэтому так бездарно выражаюсь для особ вашего сословия, зато простые пользователи меня понимают. И от меня не так сильно несет снобизмом...
Может вместо того чтобы пальцы груть, всё-таки стоит вынуть палец из ... и поделиться опытом? Сказать, как должно быть и почему, вместо соплей на кабинку... пальцем... в детском саду...
Или вообще, быть выше всего этого и не обращать внимания на статьи подобного рода, которые предназначены для простых обывателей? Ну, как минимум, чтоб корона не падала с головы...
О боже, какой максимализм. Я просто прошу разъяснить мне, что же вы хотели сказать этим предложением.
hazzik, я всего лишь подтвердил ваши слова, сказанные для Andy в одном из комментариев, потому что ... "EF (даже CodeFirst) очень глубоко пускает корни" ... это чистая правда...
именно эту часть вашего сообщения я и попытался высказать в моем комменте про промежуточный уровень абстракции...
"Но в силу того что, на данный момент, Code First поддерживает только EF, то промежуточный уровень абстракции (репозитории) по большому счету, не имеет смысла. Ибо нет альтернативы, что применить абстрагирование от конкретной ORM."
Репозитории всегда имеют смысл - это своего рода абстрация над источником данных (ORM). К примеру захочу я изменить источник данных (WCF например) или "фейкнуть" его для целей тестирования- я просто подсуну другую реализацию IRepository и вся моя бизнес-логика продолжить работать как надо.
Не совсем понял про IQueryable - понимаю, что выставлять его наружу - плохо, но как иначе можно это обойти?
Вот интересный подход QueryObject + Repository:
http://lostechies.com/chadmyers/2008/08/02/query-objects-with-repository-pattern-part-2
calabonga, у меня в проектах небольшого и среднего размера EF не вылазит за слой DAL. Что я делаю не так? :) В основе лежат POCO (правда c DataAnnotations) + 2 Fluent интерфейса - один из EF вешает его атрибуты для маппинга и один мой - вешает MVC атрибуты для вывода.
а вот по поводу IQueriable наружу это действительно некрасиво - когда надоест игратся с EF и начнете смотреть в сторону производительности тут и появятся грабли - ибо на томже petapoco и иже с ним нету iqueriable - а есть ienumerable (лично я после тестов EF положил в самый дальний угол чулана) - а так если снести IQueriable - то всегото делов переопределить inteface
P.S. я лично посмеялся от души над фразой "Но в силу того что, на данный момент, Code First поддерживает только EF" - да никто и не стремится его поддерживать
Вот как примерно у меня построено MVC приложение.
Нужно отметить, что, в моем случае возможность подменять источник данных была не хотелкой, которая введена в just for lulz, а реальной необходимостью, поэтому на самом деле схема была усложнена по сравнению с той что описываю)
Controller
Business Logic
UnitOfWork Repository
ADO EF
Database
В функции контроллера входит прием запроса, определенная обработка входящих параметров, вызов меода бизнес-логики.
1. Валидация входящих параметров (не связанная с бизнес-логикой, как правило связанная с тем что в url или POST форму могут закинуть какой-то мусор)
2. Приведение входящих параметров к виду, в котором их ожидате бизнес-логика (например, была необходимость "full" понимать как значение 100. Для этого входящий параметр метода контроллера был сделан типом string, и его значение в методе контроллера приводилось к числу).
3. Вызов метода бизнес-логики, получение результатов
4. Подготовка объекта Model (почти везде использовалась типизированная модель, для разных View создавались разные модели, соответсвующие данным которые требуются во View). Внутрь модели могли ложиться объекты DTO, целые их списки.
5. Возвращается View из метода, связанный с моделью.
Даже учитывая что в контроллерах я оставил минимум, котоырй считал возможным, они все равно получились на мой взгляд тяжеловатыми.
В бизнес-логике каждая операция реализуется завершенной транзакцией. План метода бизнес-логики выглядит примерно так
1. Валидация входящих параметров
2. Создание UnitOfWork
3. Создание репозиториев в контексте OUW
4. Получения данных через репозитории, выполнение каких-то манипуляций с данными
5. Фиксация изменений в UOW (если требуется)
6. Возвращение из метода данных (в объектах DTO)
В UnitOfWork ничего интересного, он содержит в себе контекст Entity Framework, может создавать репозитории, связанные с этим контекстом, и вызывать SaveChanges
В репозиториях в основном методы для поиска данных по определенным критериям. Возвращается IList<SomeType> или SomeType. Например
IList<User> SearchUsersByLastName(string lastName)
В репозиториях также находились методы AddObject, DeleteObject, которые добавляют/удаляют объект из контекста (но не фиксируют изменения)
IQueryable использовался очень редко, как правило в тех случаях когда приходилось делать join'ы по нескольким таблицам. Даже в этом случае IQueryable не мог выйти за рамки бизнес-логики.
В бизнес-логике могли использовать навигационные свойства EF (Order.ProductItems, например). В некоторых случаях, исходя из соображений повышения производительности, в репозиторий добавлялся метод, который делал дополнительный Include по нужным свойствам. Например, GetAllOrdersIncludeProductItems
ADO EF использовался в режиме Database First.
Бизнес-логику и контроллер можно без проблем покрыть юнит-тестами.
Репозитории, к сожалению, не получится тестировать изолированно, их скорее всего прийдется тестировать в связке с базой данных.
Наиболее сложно будет тестировать ту часть бизнес-логики где используется IQueryable (поэтому стремился использовать его по минимуму).
вобщем как-то так.
С очень небольшими изменениями на такой же архитектуре была реализована серверная часть для Silverlight приложения. Классы бизнес-логики являлись WCF сервисами.
Проблема книжки Эспозито (да и почти всех книжек и статей, которые я видел по MVC) в том что их задача - демонстрация технологии, а не хорошего патерна.
Инициализируем контекста EF в методе контроллера? Пожалуйста.
Кидаем в View IQueryable? Это можно делать, мы все равно не диспозим контекст в контроллере
Используем в View навигационные свойства? Не проблема, даже если в контроллере не позаботились о подгрузке данных, EF все сделает сам, LazyLoad же.
На практике, чтоб понять как должно быть построено MVC приложение, даже чтения Фаулера, нескольких MS-овских Patterns & Practices не было достаточно. Добивать пробелы пришлось исследуя срач коментов на 300 у какого-то блоггера, неосторожно опубликовавшего свой вариант репозитория :)
Да и Patterns & Practices не порадовали. В одном из них в качестве "Reference Implementation" предлагается веб-сайт, который вообще не хранит нигде данные.
Вам на будущее - в подобных статьях фразы типа "Это нужно сделать так, не спрашивайте почему, просто поверьте мне на слово", "Я так делаю потому что я всегда так делал" - очень слабая аргументация какой-то точки зрения. Если где-то нужно использовать HierarchicalLifetimeManager, потрудитесь объяснить почему именно. Тем более вопрос не праздный, речь идет о жизненном цикле контекста ADO EF.
Artyom Krivokrisenko, +100500
А вот интересный момент - UnitOfWork создается в BL или в Контроллере? Если в BL (как в примере Artyom Krivokrisenko), то как связать два разных действия из BL? Я из этих соображений пересен его создание в контроллер (в вызовах BL получается общий контекст, но разные субтранзакции, что позволяет при необходимости "откатить" все совершенные изменения).
Я передаю UOW в конструктор контроллера посредством DependencyResolver
Andy, вопрос интересный. В моем случае завязать несколько разных UOW в одну транзакцию не получится. Я думал об этом, но пришел к выводу, что в моем случае в этом нет необходимости. Я подходил к решению таким образом - вызов внешнего метода бизнес-логики занимает полностью одну транзакцию. Избегал разделения транзакции по нескольким методам.
В некоторых случаях, если метод бизнес-логики мог получиться очень большим, делал несколько дополнительных методов, в которые параметром передавал экземпляр UOW. Необходимость в таком редко возникала, кстати. Причем обычно в сценариях, в которых требовалось только чтение.
Инициализация UnitOfWork происходит в бизнес-логике. Причем явно, через фабрику.
public void ResetPassword(string userName, string newPassword) { if (!this.passwordQualityValidator.Validate(newPassword)) { throw new FaultException("Пароль недостаточно надежный"); } using (IUnitOfWork<Domain.Entities> unitOfWork = this.unitOfWorkFactory.Create()) { IUsersRepository usersRepository = unitOfWork.CreateRepository<IUsersRepository>(); Domain.User user = usersRepository.GetUserByUserName(userName); if (user == null) { throw new FaultException(string.Format("Пользователь {0} не зарегестрирован в базе", userName)); } string newSalt = this.GenerateSalt(this.passwordSaltLength); byte[] newPasswordHash = this.ComputePasswordHash(newPassword, newSalt); user.PasswordSalt = newSalt; user.PasswordHash = newPasswordHash; unitOfWork.SaveChanges(); } }
Andy, я ошибся, на самом деле можно сделать то, о чем вы говорите. С помощью TransactionScope. Возможно, использовать его не напрямую, а через абстракцию, чтоб можно было это потом тестировать. Такой варант я рассматривал, но как сказал выше, до реализации дело не дошло.
Skyliver, вариант с автоматической инициализацией UOW я рассматривал тоже, но отказался от него в пользу инициализации по запросу.
Причины - неопределенность жизненного цикла UOW. Неопределенность с передачей UOW между разными частями приложения. Делая какую-то функцию в бизнес-логике, нужно понимать что тот же самый UOW мог использоваться выше или ниже по стеку. Потенциальная рассогласованность данных в рамках одного UOW.
С ручной инициализацией все проще. Метод полностью владеет своим выделенным UOW, сам решает, необходимо ли с кем-то этим UOW "поделитья", передав его туда как аргумент, или же использовать эксклюзивно.
Вот пример меторда репозитория для предыдущего примера
namespace Granite.Core.Domain.Repositories { using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; using Granite.Core.Domain.Contracts; public class UsersRepository : IUsersRepository { private readonly Entities entities; public UsersRepository(Entities entities) { this.entities = entities; } public void Add(User user) { this.entities.Users.AddObject(user); } private IQueryable<User> VisibleUsers { get { return this.entities.Users.Where(u => !u.IsDeleted); } } public IList<User> GetAllUsers() { IQueryable<User> users = this.VisibleUsers; return users.ToList(); } public bool UserNameExists(string userName) { return this.entities.Users.Any(u => u.UserName == userName); } public User GetUserByUserName(string userName) { return this.VisibleUsers.SingleOrDefault(u => u.UserName == userName); } public User GetUserById(Guid id) { return this.VisibleUsers.SingleOrDefault(u => u.Id == id); } } }
Добрый день!
Подскажите пожалуйста, работают ли POCO без создания ADO.NET Entity DataModel?
Валера, не очень понимаю ваш вопрос. На самом деле, POCO классы могут были или моделями или их проекцией (proxy)... Уточните вопрос пожалуйста.
calabonga, я создал poco объекты и контекст так как описано в этой статье http://blogs.msdn.com/b/adonet/archive/2011/09/28/ef-4-2-model-amp-database-first-walkthrough.aspx . Если затем удалить edmx файл, poco перестает работать.
Валера, я мог бы у вас лично попросить прощения, но я не очень понимаю, в чем конкретно я ошибся? Пожалуйста, помогите!
calabonga, почему вы ошиблись? Вы написали отличную статью, спасибо!
Это был мой вопрос относительно работы с poco вообще. Может ли poco работать без создания ADO.NET Entity DataModel?
Валера, а что тогда значит "перестают работать"? POCO - объекты, которые бесмысленны без БД (EF или еще какая-нибудь). POCO - есть прокси-классы объектов БД.... Это если говорить простым языком. Или я тогда просто не пойму, зачемы вы их создавали? Или зачем тогда EF удалили?
calabonga, я вобщем то уже разобрался. Просто сначала подумал, что можно работать с poco без создания edmx файла.
У меня еще есть вопрос. Допустим у нас есть библиотека классов (слой данных) в которой создана ADO.NET Entity DataModel модель. Затем сгенерированы poco классы и контекст данных. Вопрос - как вынести poco объекты в другую библиотеку (бизнес логика) и стоит ли так делать?
Поясню идею: Есть решение в которое входит 3 проекта. 1) Библиотека с EF , 2) Библиотека с бизнес логикой 3) ASP.NET MVC приложение. Задача заключается в том, чтобы построить архитектуру при которой №3 будет зависеть от №2 , а номер №2 будет зависеть от №1. То есть в слое №3 нельзя создавать объекты EF из слоя №1. Как думаете на сколько правилен такой подход?
Валера, мне кажется нет особого смысла разделять POCO и EF. В одном из рабочих проектов мы так сделали, но, я уже не могу примнить почему, пришлось "склеить" всё обратно.
calabonga, не подскажите еще по одному вопросу..
Есть ASP.NET MVC 4 проект. В проекте есть WebApi контроллер который возвращает набор типов, например IEnumerable<Cat>. То есть происходит get запрос в ответ на который отправляется json. Так вот, как быть если объект Cat содержит свойство типа CatType (это такой тип с полями id и name). Ведь poco объект не загрузит связанные данные поэтому после сериализации в json поле CatType будет иметь значение null. Если перед сериализацией заполнить это поле в цикле, то произойдет ошибка. Как решить подобную проблему?
Валера, по идее, нужно сериализовать объект, например, поставить атрибуты. Но у меня даже так не получилось, сериализатор "падал" в рекурсию... В интеренете нет информации.. Я жду теперь релиза.
Ясно, будем ждать
Добрый вечер, хочу поблагодарить за данный проект. Возник небольшой вопросик по данной теме, а именно: в классе репозитория
>>> ...
public void Delete(int id) { Hall h = this.Find(id); if (h != null) { Save(); } }
...
нужна ли, перед Save() строка: context.Halls.Remove(h); ?
заранее спасибо
Да, Max, вы действительно правы! Именно такая строка и должна быть, вот что значит "вручную" делать репозитории. Совсем отвык.
Спасибо. исправил.
Прошу прощения, очень нравиться материал, но как читать комментарии они у меня обрезаны(
Здравствуйте. Подскажите, не работает авторизация. Всё сделал, как по вашему примеру. В Web Site Administration Tool добавляю пользователя, но когда пытаюсь залогиниться в своём приложении, говорит что такого пользователя нет. В созданной базе данных тоже не появляется информация о созданном пользователе. Не понятно где Web Site Administration Tool сохраняет своих пользователей. Что странно, когда регистрируюсь в моём приложении MVC всё работает и данные сохраняются в моей базе?
Как подключить Web Site Administration Tool тоже к моей базе?
П.С. Спасибо за статьи. Очень доходчиво и интересно. Сейчас делаю свой проект и мне они очень помагают :)
Если вы делали по статье, то у вас должен быть ASP.NET MVC 3, а не MVC 4, и тогда всё должно получится. В противном случае, если вы использовали шаблон проекта MVC 4, ничего не выйдет с Web Site Administration Tool, ибо он использует SqlMembershipProvider. В MVC 4 по умолчанию в шаблоне проекта испольуется SimpleMembershipProvider. Эти типы провадеров никак не связаны. Web Site Administration Tool - работает с устаревшим провайдером (пришел еще с ASP.NET v1.0), а в MVC 4 используется новый.
Резюме: Или настройте свой проект на использование устаревшего провайдера SqlMembershipProvider чтобы работать с устаревшим инструментом Web Site Administration Tool, или используйте новый провайдер, в который можно просто добавить пользователя в таблице (как и роли, так и роли для пользователя).